PageRenderTime 68ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 1ms

/SQLAlchemy-0.7.8/test/dialect/test_mysql.py

#
Python | 1668 lines | 1628 code | 25 blank | 15 comment | 12 complexity | 3976419ae171c99a680c2eef9e5df809 MD5 | raw file
  1. # coding: utf-8
  2. from test.lib.testing import eq_, assert_raises, assert_raises_message
  3. # Py2K
  4. import sets
  5. # end Py2K
  6. from sqlalchemy import *
  7. from sqlalchemy import sql, exc, schema, types as sqltypes, event
  8. from sqlalchemy.dialects.mysql import base as mysql
  9. from sqlalchemy.engine.url import make_url
  10. from test.lib.testing import eq_
  11. from test.lib import *
  12. from test.lib.engines import utf8_engine
  13. import datetime
  14. class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
  15. __dialect__ = mysql.dialect()
  16. def test_reserved_words(self):
  17. table = Table("mysql_table", MetaData(),
  18. Column("col1", Integer),
  19. Column("master_ssl_verify_server_cert", Integer))
  20. x = select([table.c.col1, table.c.master_ssl_verify_server_cert])
  21. self.assert_compile(x,
  22. '''SELECT mysql_table.col1, mysql_table.`master_ssl_verify_server_cert` FROM mysql_table''')
  23. def test_create_index_simple(self):
  24. m = MetaData()
  25. tbl = Table('testtbl', m, Column('data', String(255)))
  26. idx = Index('test_idx1', tbl.c.data)
  27. self.assert_compile(schema.CreateIndex(idx),
  28. 'CREATE INDEX test_idx1 ON testtbl (data)',
  29. dialect=mysql.dialect())
  30. def test_create_index_with_length(self):
  31. m = MetaData()
  32. tbl = Table('testtbl', m, Column('data', String(255)))
  33. idx1 = Index('test_idx1', tbl.c.data, mysql_length=10)
  34. idx2 = Index('test_idx2', tbl.c.data, mysql_length=5)
  35. self.assert_compile(schema.CreateIndex(idx1),
  36. 'CREATE INDEX test_idx1 ON testtbl (data(10))',
  37. dialect=mysql.dialect())
  38. self.assert_compile(schema.CreateIndex(idx2),
  39. 'CREATE INDEX test_idx2 ON testtbl (data(5))',
  40. dialect=mysql.dialect())
  41. def test_create_index_with_using(self):
  42. m = MetaData()
  43. tbl = Table('testtbl', m, Column('data', String(255)))
  44. idx1 = Index('test_idx1', tbl.c.data, mysql_using='btree')
  45. idx2 = Index('test_idx2', tbl.c.data, mysql_using='hash')
  46. self.assert_compile(schema.CreateIndex(idx1),
  47. 'CREATE INDEX test_idx1 ON testtbl (data) USING btree',
  48. dialect=mysql.dialect())
  49. self.assert_compile(schema.CreateIndex(idx2),
  50. 'CREATE INDEX test_idx2 ON testtbl (data) USING hash',
  51. dialect=mysql.dialect())
  52. def test_create_pk_plain(self):
  53. m = MetaData()
  54. tbl = Table('testtbl', m, Column('data', String(255)),
  55. PrimaryKeyConstraint('data'))
  56. self.assert_compile(schema.CreateTable(tbl),
  57. "CREATE TABLE testtbl (data VARCHAR(255), PRIMARY KEY (data))",
  58. dialect=mysql.dialect())
  59. def test_create_pk_with_using(self):
  60. m = MetaData()
  61. tbl = Table('testtbl', m, Column('data', String(255)),
  62. PrimaryKeyConstraint('data', mysql_using='btree'))
  63. self.assert_compile(schema.CreateTable(tbl),
  64. "CREATE TABLE testtbl (data VARCHAR(255), "
  65. "PRIMARY KEY (data) USING btree)",
  66. dialect=mysql.dialect())
  67. class DialectTest(fixtures.TestBase):
  68. __only_on__ = 'mysql'
  69. @testing.only_on(['mysql+mysqldb', 'mysql+oursql'],
  70. 'requires particular SSL arguments')
  71. def test_ssl_arguments(self):
  72. dialect = testing.db.dialect
  73. kwarg = dialect.create_connect_args(
  74. make_url("mysql://scott:tiger@localhost:3306/test"
  75. "?ssl_ca=/ca.pem&ssl_cert=/cert.pem&ssl_key=/key.pem")
  76. )[1]
  77. # args that differ among mysqldb and oursql
  78. for k in ('use_unicode', 'found_rows', 'client_flag'):
  79. kwarg.pop(k, None)
  80. eq_(
  81. kwarg,
  82. {
  83. 'passwd': 'tiger', 'db': 'test',
  84. 'ssl': {'ca': '/ca.pem', 'cert': '/cert.pem',
  85. 'key': '/key.pem'},
  86. 'host': 'localhost', 'user': 'scott',
  87. 'port': 3306
  88. }
  89. )
  90. class TypesTest(fixtures.TestBase, AssertsExecutionResults, AssertsCompiledSQL):
  91. "Test MySQL column types"
  92. __only_on__ = 'mysql'
  93. __dialect__ = mysql.dialect()
  94. @testing.uses_deprecated('Manually quoting ENUM value literals')
  95. def test_basic(self):
  96. meta1 = MetaData(testing.db)
  97. table = Table(
  98. 'mysql_types', meta1,
  99. Column('id', Integer, primary_key=True),
  100. Column('num1', mysql.MSInteger(unsigned=True)),
  101. Column('text1', mysql.MSLongText),
  102. Column('text2', mysql.MSLongText()),
  103. Column('num2', mysql.MSBigInteger),
  104. Column('num3', mysql.MSBigInteger()),
  105. Column('num4', mysql.MSDouble),
  106. Column('num5', mysql.MSDouble()),
  107. Column('num6', mysql.MSMediumInteger),
  108. Column('enum1', mysql.ENUM("'black'", "'white'")),
  109. Column('enum2', mysql.ENUM("dog", "cat")),
  110. )
  111. try:
  112. table.drop(checkfirst=True)
  113. table.create()
  114. meta2 = MetaData(testing.db)
  115. t2 = Table('mysql_types', meta2, autoload=True)
  116. assert isinstance(t2.c.num1.type, mysql.MSInteger)
  117. assert t2.c.num1.type.unsigned
  118. assert isinstance(t2.c.text1.type, mysql.MSLongText)
  119. assert isinstance(t2.c.text2.type, mysql.MSLongText)
  120. assert isinstance(t2.c.num2.type, mysql.MSBigInteger)
  121. assert isinstance(t2.c.num3.type, mysql.MSBigInteger)
  122. assert isinstance(t2.c.num4.type, mysql.MSDouble)
  123. assert isinstance(t2.c.num5.type, mysql.MSDouble)
  124. assert isinstance(t2.c.num6.type, mysql.MSMediumInteger)
  125. assert isinstance(t2.c.enum1.type, mysql.ENUM)
  126. assert isinstance(t2.c.enum2.type, mysql.ENUM)
  127. t2.drop()
  128. t2.create()
  129. finally:
  130. meta1.drop_all()
  131. def test_numeric(self):
  132. "Exercise type specification and options for numeric types."
  133. columns = [
  134. # column type, args, kwargs, expected ddl
  135. # e.g. Column(Integer(10, unsigned=True)) ==
  136. # 'INTEGER(10) UNSIGNED'
  137. (mysql.MSNumeric, [], {},
  138. 'NUMERIC'),
  139. (mysql.MSNumeric, [None], {},
  140. 'NUMERIC'),
  141. (mysql.MSNumeric, [12], {},
  142. 'NUMERIC(12)'),
  143. (mysql.MSNumeric, [12, 4], {'unsigned':True},
  144. 'NUMERIC(12, 4) UNSIGNED'),
  145. (mysql.MSNumeric, [12, 4], {'zerofill':True},
  146. 'NUMERIC(12, 4) ZEROFILL'),
  147. (mysql.MSNumeric, [12, 4], {'zerofill':True, 'unsigned':True},
  148. 'NUMERIC(12, 4) UNSIGNED ZEROFILL'),
  149. (mysql.MSDecimal, [], {},
  150. 'DECIMAL'),
  151. (mysql.MSDecimal, [None], {},
  152. 'DECIMAL'),
  153. (mysql.MSDecimal, [12], {},
  154. 'DECIMAL(12)'),
  155. (mysql.MSDecimal, [12, None], {},
  156. 'DECIMAL(12)'),
  157. (mysql.MSDecimal, [12, 4], {'unsigned':True},
  158. 'DECIMAL(12, 4) UNSIGNED'),
  159. (mysql.MSDecimal, [12, 4], {'zerofill':True},
  160. 'DECIMAL(12, 4) ZEROFILL'),
  161. (mysql.MSDecimal, [12, 4], {'zerofill':True, 'unsigned':True},
  162. 'DECIMAL(12, 4) UNSIGNED ZEROFILL'),
  163. (mysql.MSDouble, [None, None], {},
  164. 'DOUBLE'),
  165. (mysql.MSDouble, [12, 4], {'unsigned':True},
  166. 'DOUBLE(12, 4) UNSIGNED'),
  167. (mysql.MSDouble, [12, 4], {'zerofill':True},
  168. 'DOUBLE(12, 4) ZEROFILL'),
  169. (mysql.MSDouble, [12, 4], {'zerofill':True, 'unsigned':True},
  170. 'DOUBLE(12, 4) UNSIGNED ZEROFILL'),
  171. (mysql.MSReal, [None, None], {},
  172. 'REAL'),
  173. (mysql.MSReal, [12, 4], {'unsigned':True},
  174. 'REAL(12, 4) UNSIGNED'),
  175. (mysql.MSReal, [12, 4], {'zerofill':True},
  176. 'REAL(12, 4) ZEROFILL'),
  177. (mysql.MSReal, [12, 4], {'zerofill':True, 'unsigned':True},
  178. 'REAL(12, 4) UNSIGNED ZEROFILL'),
  179. (mysql.MSFloat, [], {},
  180. 'FLOAT'),
  181. (mysql.MSFloat, [None], {},
  182. 'FLOAT'),
  183. (mysql.MSFloat, [12], {},
  184. 'FLOAT(12)'),
  185. (mysql.MSFloat, [12, 4], {},
  186. 'FLOAT(12, 4)'),
  187. (mysql.MSFloat, [12, 4], {'unsigned':True},
  188. 'FLOAT(12, 4) UNSIGNED'),
  189. (mysql.MSFloat, [12, 4], {'zerofill':True},
  190. 'FLOAT(12, 4) ZEROFILL'),
  191. (mysql.MSFloat, [12, 4], {'zerofill':True, 'unsigned':True},
  192. 'FLOAT(12, 4) UNSIGNED ZEROFILL'),
  193. (mysql.MSInteger, [], {},
  194. 'INTEGER'),
  195. (mysql.MSInteger, [4], {},
  196. 'INTEGER(4)'),
  197. (mysql.MSInteger, [4], {'unsigned':True},
  198. 'INTEGER(4) UNSIGNED'),
  199. (mysql.MSInteger, [4], {'zerofill':True},
  200. 'INTEGER(4) ZEROFILL'),
  201. (mysql.MSInteger, [4], {'zerofill':True, 'unsigned':True},
  202. 'INTEGER(4) UNSIGNED ZEROFILL'),
  203. (mysql.MSBigInteger, [], {},
  204. 'BIGINT'),
  205. (mysql.MSBigInteger, [4], {},
  206. 'BIGINT(4)'),
  207. (mysql.MSBigInteger, [4], {'unsigned':True},
  208. 'BIGINT(4) UNSIGNED'),
  209. (mysql.MSBigInteger, [4], {'zerofill':True},
  210. 'BIGINT(4) ZEROFILL'),
  211. (mysql.MSBigInteger, [4], {'zerofill':True, 'unsigned':True},
  212. 'BIGINT(4) UNSIGNED ZEROFILL'),
  213. (mysql.MSMediumInteger, [], {},
  214. 'MEDIUMINT'),
  215. (mysql.MSMediumInteger, [4], {},
  216. 'MEDIUMINT(4)'),
  217. (mysql.MSMediumInteger, [4], {'unsigned':True},
  218. 'MEDIUMINT(4) UNSIGNED'),
  219. (mysql.MSMediumInteger, [4], {'zerofill':True},
  220. 'MEDIUMINT(4) ZEROFILL'),
  221. (mysql.MSMediumInteger, [4], {'zerofill':True, 'unsigned':True},
  222. 'MEDIUMINT(4) UNSIGNED ZEROFILL'),
  223. (mysql.MSTinyInteger, [], {},
  224. 'TINYINT'),
  225. (mysql.MSTinyInteger, [1], {},
  226. 'TINYINT(1)'),
  227. (mysql.MSTinyInteger, [1], {'unsigned':True},
  228. 'TINYINT(1) UNSIGNED'),
  229. (mysql.MSTinyInteger, [1], {'zerofill':True},
  230. 'TINYINT(1) ZEROFILL'),
  231. (mysql.MSTinyInteger, [1], {'zerofill':True, 'unsigned':True},
  232. 'TINYINT(1) UNSIGNED ZEROFILL'),
  233. (mysql.MSSmallInteger, [], {},
  234. 'SMALLINT'),
  235. (mysql.MSSmallInteger, [4], {},
  236. 'SMALLINT(4)'),
  237. (mysql.MSSmallInteger, [4], {'unsigned':True},
  238. 'SMALLINT(4) UNSIGNED'),
  239. (mysql.MSSmallInteger, [4], {'zerofill':True},
  240. 'SMALLINT(4) ZEROFILL'),
  241. (mysql.MSSmallInteger, [4], {'zerofill':True, 'unsigned':True},
  242. 'SMALLINT(4) UNSIGNED ZEROFILL'),
  243. ]
  244. table_args = ['test_mysql_numeric', MetaData(testing.db)]
  245. for index, spec in enumerate(columns):
  246. type_, args, kw, res = spec
  247. table_args.append(Column('c%s' % index, type_(*args, **kw)))
  248. numeric_table = Table(*table_args)
  249. gen = testing.db.dialect.ddl_compiler(testing.db.dialect, None)
  250. for col in numeric_table.c:
  251. index = int(col.name[1:])
  252. eq_(gen.get_column_specification(col),
  253. "%s %s" % (col.name, columns[index][3]))
  254. self.assert_(repr(col))
  255. try:
  256. numeric_table.create(checkfirst=True)
  257. assert True
  258. except:
  259. raise
  260. numeric_table.drop()
  261. @testing.exclude('mysql', '<', (4, 1, 1), 'no charset support')
  262. def test_charset(self):
  263. """Exercise CHARACTER SET and COLLATE-ish options on string types."""
  264. columns = [
  265. (mysql.MSChar, [1], {},
  266. 'CHAR(1)'),
  267. (mysql.NCHAR, [1], {},
  268. 'NATIONAL CHAR(1)'),
  269. (mysql.MSChar, [1], {'binary':True},
  270. 'CHAR(1) BINARY'),
  271. (mysql.MSChar, [1], {'ascii':True},
  272. 'CHAR(1) ASCII'),
  273. (mysql.MSChar, [1], {'unicode':True},
  274. 'CHAR(1) UNICODE'),
  275. (mysql.MSChar, [1], {'ascii':True, 'binary':True},
  276. 'CHAR(1) ASCII BINARY'),
  277. (mysql.MSChar, [1], {'unicode':True, 'binary':True},
  278. 'CHAR(1) UNICODE BINARY'),
  279. (mysql.MSChar, [1], {'charset':'utf8'},
  280. 'CHAR(1) CHARACTER SET utf8'),
  281. (mysql.MSChar, [1], {'charset':'utf8', 'binary':True},
  282. 'CHAR(1) CHARACTER SET utf8 BINARY'),
  283. (mysql.MSChar, [1], {'charset':'utf8', 'unicode':True},
  284. 'CHAR(1) CHARACTER SET utf8'),
  285. (mysql.MSChar, [1], {'charset':'utf8', 'ascii':True},
  286. 'CHAR(1) CHARACTER SET utf8'),
  287. (mysql.MSChar, [1], {'collation': 'utf8_bin'},
  288. 'CHAR(1) COLLATE utf8_bin'),
  289. (mysql.MSChar, [1], {'charset': 'utf8', 'collation': 'utf8_bin'},
  290. 'CHAR(1) CHARACTER SET utf8 COLLATE utf8_bin'),
  291. (mysql.MSChar, [1], {'charset': 'utf8', 'binary': True},
  292. 'CHAR(1) CHARACTER SET utf8 BINARY'),
  293. (mysql.MSChar, [1], {'charset': 'utf8', 'collation': 'utf8_bin',
  294. 'binary': True},
  295. 'CHAR(1) CHARACTER SET utf8 COLLATE utf8_bin'),
  296. (mysql.MSChar, [1], {'national':True},
  297. 'NATIONAL CHAR(1)'),
  298. (mysql.MSChar, [1], {'national':True, 'charset':'utf8'},
  299. 'NATIONAL CHAR(1)'),
  300. (mysql.MSChar, [1], {'national':True, 'charset':'utf8',
  301. 'binary':True},
  302. 'NATIONAL CHAR(1) BINARY'),
  303. (mysql.MSChar, [1], {'national':True, 'binary':True,
  304. 'unicode':True},
  305. 'NATIONAL CHAR(1) BINARY'),
  306. (mysql.MSChar, [1], {'national':True, 'collation':'utf8_bin'},
  307. 'NATIONAL CHAR(1) COLLATE utf8_bin'),
  308. (mysql.MSString, [1], {'charset':'utf8', 'collation':'utf8_bin'},
  309. 'VARCHAR(1) CHARACTER SET utf8 COLLATE utf8_bin'),
  310. (mysql.MSString, [1], {'national':True, 'collation':'utf8_bin'},
  311. 'NATIONAL VARCHAR(1) COLLATE utf8_bin'),
  312. (mysql.MSTinyText, [], {'charset':'utf8', 'collation':'utf8_bin'},
  313. 'TINYTEXT CHARACTER SET utf8 COLLATE utf8_bin'),
  314. (mysql.MSMediumText, [], {'charset':'utf8', 'binary':True},
  315. 'MEDIUMTEXT CHARACTER SET utf8 BINARY'),
  316. (mysql.MSLongText, [], {'ascii':True},
  317. 'LONGTEXT ASCII'),
  318. (mysql.ENUM, ["foo", "bar"], {'unicode':True},
  319. '''ENUM('foo','bar') UNICODE''')
  320. ]
  321. table_args = ['test_mysql_charset', MetaData(testing.db)]
  322. for index, spec in enumerate(columns):
  323. type_, args, kw, res = spec
  324. table_args.append(Column('c%s' % index, type_(*args, **kw)))
  325. charset_table = Table(*table_args)
  326. gen = testing.db.dialect.ddl_compiler(testing.db.dialect, None)
  327. for col in charset_table.c:
  328. index = int(col.name[1:])
  329. eq_(gen.get_column_specification(col),
  330. "%s %s" % (col.name, columns[index][3]))
  331. self.assert_(repr(col))
  332. try:
  333. charset_table.create(checkfirst=True)
  334. assert True
  335. except:
  336. raise
  337. charset_table.drop()
  338. @testing.exclude('mysql', '<', (5, 0, 5), 'a 5.0+ feature')
  339. @testing.provide_metadata
  340. def test_charset_collate_table(self):
  341. t = Table('foo', self.metadata,
  342. Column('id', Integer),
  343. mysql_default_charset='utf8',
  344. mysql_collate='utf8_unicode_ci'
  345. )
  346. t.create()
  347. m2 = MetaData(testing.db)
  348. t2 = Table('foo', m2, autoload=True)
  349. eq_(t2.kwargs['mysql_collate'], 'utf8_unicode_ci')
  350. eq_(t2.kwargs['mysql_default charset'], 'utf8')
  351. @testing.exclude('mysql', '<', (5, 0, 5), 'a 5.0+ feature')
  352. @testing.fails_on('mysql+oursql', 'some round trips fail, oursql bug ?')
  353. def test_bit_50(self):
  354. """Exercise BIT types on 5.0+ (not valid for all engine types)"""
  355. meta = MetaData(testing.db)
  356. bit_table = Table('mysql_bits', meta,
  357. Column('b1', mysql.MSBit),
  358. Column('b2', mysql.MSBit()),
  359. Column('b3', mysql.MSBit(), nullable=False),
  360. Column('b4', mysql.MSBit(1)),
  361. Column('b5', mysql.MSBit(8)),
  362. Column('b6', mysql.MSBit(32)),
  363. Column('b7', mysql.MSBit(63)),
  364. Column('b8', mysql.MSBit(64)))
  365. eq_(colspec(bit_table.c.b1), 'b1 BIT')
  366. eq_(colspec(bit_table.c.b2), 'b2 BIT')
  367. eq_(colspec(bit_table.c.b3), 'b3 BIT NOT NULL')
  368. eq_(colspec(bit_table.c.b4), 'b4 BIT(1)')
  369. eq_(colspec(bit_table.c.b5), 'b5 BIT(8)')
  370. eq_(colspec(bit_table.c.b6), 'b6 BIT(32)')
  371. eq_(colspec(bit_table.c.b7), 'b7 BIT(63)')
  372. eq_(colspec(bit_table.c.b8), 'b8 BIT(64)')
  373. for col in bit_table.c:
  374. self.assert_(repr(col))
  375. try:
  376. meta.create_all()
  377. meta2 = MetaData(testing.db)
  378. reflected = Table('mysql_bits', meta2, autoload=True)
  379. for table in bit_table, reflected:
  380. def roundtrip(store, expected=None):
  381. expected = expected or store
  382. table.insert(store).execute()
  383. row = table.select().execute().first()
  384. try:
  385. self.assert_(list(row) == expected)
  386. except:
  387. print "Storing %s" % store
  388. print "Expected %s" % expected
  389. print "Found %s" % list(row)
  390. raise
  391. table.delete().execute().close()
  392. roundtrip([0] * 8)
  393. roundtrip([None, None, 0, None, None, None, None, None])
  394. roundtrip([1] * 8)
  395. roundtrip([sql.text("b'1'")] * 8, [1] * 8)
  396. i = 255
  397. roundtrip([0, 0, 0, 0, i, i, i, i])
  398. i = 2**32 - 1
  399. roundtrip([0, 0, 0, 0, 0, i, i, i])
  400. i = 2**63 - 1
  401. roundtrip([0, 0, 0, 0, 0, 0, i, i])
  402. i = 2**64 - 1
  403. roundtrip([0, 0, 0, 0, 0, 0, 0, i])
  404. finally:
  405. meta.drop_all()
  406. def test_boolean(self):
  407. """Test BOOL/TINYINT(1) compatibility and reflection."""
  408. meta = MetaData(testing.db)
  409. bool_table = Table(
  410. 'mysql_bool',
  411. meta,
  412. Column('b1', BOOLEAN),
  413. Column('b2', Boolean),
  414. Column('b3', mysql.MSTinyInteger(1)),
  415. Column('b4', mysql.MSTinyInteger(1, unsigned=True)),
  416. Column('b5', mysql.MSTinyInteger),
  417. )
  418. eq_(colspec(bool_table.c.b1), 'b1 BOOL')
  419. eq_(colspec(bool_table.c.b2), 'b2 BOOL')
  420. eq_(colspec(bool_table.c.b3), 'b3 TINYINT(1)')
  421. eq_(colspec(bool_table.c.b4), 'b4 TINYINT(1) UNSIGNED')
  422. eq_(colspec(bool_table.c.b5), 'b5 TINYINT')
  423. for col in bool_table.c:
  424. self.assert_(repr(col))
  425. try:
  426. meta.create_all()
  427. table = bool_table
  428. def roundtrip(store, expected=None):
  429. expected = expected or store
  430. table.insert(store).execute()
  431. row = table.select().execute().first()
  432. try:
  433. self.assert_(list(row) == expected)
  434. for i, val in enumerate(expected):
  435. if isinstance(val, bool):
  436. self.assert_(val is row[i])
  437. except:
  438. print 'Storing %s' % store
  439. print 'Expected %s' % expected
  440. print 'Found %s' % list(row)
  441. raise
  442. table.delete().execute().close()
  443. roundtrip([None, None, None, None, None])
  444. roundtrip([True, True, 1, 1, 1])
  445. roundtrip([False, False, 0, 0, 0])
  446. roundtrip([True, True, True, True, True], [True, True, 1,
  447. 1, 1])
  448. roundtrip([False, False, 0, 0, 0], [False, False, 0, 0, 0])
  449. meta2 = MetaData(testing.db)
  450. table = Table('mysql_bool', meta2, autoload=True)
  451. eq_(colspec(table.c.b3), 'b3 TINYINT(1)')
  452. eq_(colspec(table.c.b4), 'b4 TINYINT(1) UNSIGNED')
  453. meta2 = MetaData(testing.db)
  454. table = Table(
  455. 'mysql_bool',
  456. meta2,
  457. Column('b1', BOOLEAN),
  458. Column('b2', Boolean),
  459. Column('b3', BOOLEAN),
  460. Column('b4', BOOLEAN),
  461. autoload=True,
  462. )
  463. eq_(colspec(table.c.b3), 'b3 BOOL')
  464. eq_(colspec(table.c.b4), 'b4 BOOL')
  465. roundtrip([None, None, None, None, None])
  466. roundtrip([True, True, 1, 1, 1], [True, True, True, True,
  467. 1])
  468. roundtrip([False, False, 0, 0, 0], [False, False, False,
  469. False, 0])
  470. roundtrip([True, True, True, True, True], [True, True,
  471. True, True, 1])
  472. roundtrip([False, False, 0, 0, 0], [False, False, False,
  473. False, 0])
  474. finally:
  475. meta.drop_all()
  476. @testing.exclude('mysql', '<', (4, 1, 0), '4.1+ syntax')
  477. def test_timestamp(self):
  478. """Exercise funky TIMESTAMP default syntax."""
  479. meta = MetaData(testing.db)
  480. try:
  481. columns = [
  482. ([TIMESTAMP],
  483. 'TIMESTAMP NULL'),
  484. ([mysql.MSTimeStamp],
  485. 'TIMESTAMP NULL'),
  486. ([mysql.MSTimeStamp,
  487. DefaultClause(sql.text('CURRENT_TIMESTAMP'))],
  488. "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"),
  489. ([mysql.MSTimeStamp,
  490. DefaultClause(sql.text("'1999-09-09 09:09:09'"))],
  491. "TIMESTAMP DEFAULT '1999-09-09 09:09:09'"),
  492. ([mysql.MSTimeStamp,
  493. DefaultClause(sql.text("'1999-09-09 09:09:09' "
  494. "ON UPDATE CURRENT_TIMESTAMP"))],
  495. "TIMESTAMP DEFAULT '1999-09-09 09:09:09' "
  496. "ON UPDATE CURRENT_TIMESTAMP"),
  497. ([mysql.MSTimeStamp,
  498. DefaultClause(sql.text("CURRENT_TIMESTAMP "
  499. "ON UPDATE CURRENT_TIMESTAMP"))],
  500. "TIMESTAMP DEFAULT CURRENT_TIMESTAMP "
  501. "ON UPDATE CURRENT_TIMESTAMP"),
  502. ]
  503. for idx, (spec, expected) in enumerate(columns):
  504. t = Table('mysql_ts%s' % idx, meta,
  505. Column('id', Integer, primary_key=True),
  506. Column('t', *spec))
  507. eq_(colspec(t.c.t), "t %s" % expected)
  508. self.assert_(repr(t.c.t))
  509. t.create()
  510. r = Table('mysql_ts%s' % idx, MetaData(testing.db),
  511. autoload=True)
  512. if len(spec) > 1:
  513. self.assert_(r.c.t is not None)
  514. finally:
  515. meta.drop_all()
  516. def test_timestamp_nullable(self):
  517. meta = MetaData(testing.db)
  518. ts_table = Table('mysql_timestamp', meta,
  519. Column('t1', TIMESTAMP),
  520. Column('t2', TIMESTAMP, nullable=False),
  521. )
  522. meta.create_all()
  523. try:
  524. # there's a slight assumption here that this test can
  525. # complete within the scope of a single second.
  526. # if needed, can break out the eq_() just to check for
  527. # timestamps that are within a few seconds of "now"
  528. # using timedelta.
  529. now = testing.db.execute("select now()").scalar()
  530. # TIMESTAMP without NULL inserts current time when passed
  531. # NULL. when not passed, generates 0000-00-00 quite
  532. # annoyingly.
  533. ts_table.insert().execute({'t1':now, 't2':None})
  534. ts_table.insert().execute({'t1':None, 't2':None})
  535. eq_(
  536. ts_table.select().execute().fetchall(),
  537. [(now, now), (None, now)]
  538. )
  539. finally:
  540. meta.drop_all()
  541. def test_year(self):
  542. """Exercise YEAR."""
  543. meta = MetaData(testing.db)
  544. year_table = Table('mysql_year', meta,
  545. Column('y1', mysql.MSYear),
  546. Column('y2', mysql.MSYear),
  547. Column('y3', mysql.MSYear),
  548. Column('y4', mysql.MSYear(2)),
  549. Column('y5', mysql.MSYear(4)))
  550. for col in year_table.c:
  551. self.assert_(repr(col))
  552. try:
  553. year_table.create()
  554. reflected = Table('mysql_year', MetaData(testing.db),
  555. autoload=True)
  556. for table in year_table, reflected:
  557. table.insert(['1950', '50', None, 50, 1950]).execute()
  558. row = table.select().execute().first()
  559. eq_(list(row), [1950, 2050, None, 50, 1950])
  560. table.delete().execute()
  561. self.assert_(colspec(table.c.y1).startswith('y1 YEAR'))
  562. eq_(colspec(table.c.y4), 'y4 YEAR(2)')
  563. eq_(colspec(table.c.y5), 'y5 YEAR(4)')
  564. finally:
  565. meta.drop_all()
  566. def test_set(self):
  567. """Exercise the SET type."""
  568. meta = MetaData(testing.db)
  569. set_table = Table('mysql_set', meta, Column('s1',
  570. mysql.MSSet("'dq'", "'sq'")), Column('s2',
  571. mysql.MSSet("'a'")), Column('s3',
  572. mysql.MSSet("'5'", "'7'", "'9'")))
  573. eq_(colspec(set_table.c.s1), "s1 SET('dq','sq')")
  574. eq_(colspec(set_table.c.s2), "s2 SET('a')")
  575. eq_(colspec(set_table.c.s3), "s3 SET('5','7','9')")
  576. for col in set_table.c:
  577. self.assert_(repr(col))
  578. try:
  579. set_table.create()
  580. reflected = Table('mysql_set', MetaData(testing.db),
  581. autoload=True)
  582. for table in set_table, reflected:
  583. def roundtrip(store, expected=None):
  584. expected = expected or store
  585. table.insert(store).execute()
  586. row = table.select().execute().first()
  587. try:
  588. self.assert_(list(row) == expected)
  589. except:
  590. print 'Storing %s' % store
  591. print 'Expected %s' % expected
  592. print 'Found %s' % list(row)
  593. raise
  594. table.delete().execute()
  595. roundtrip([None, None, None], [None] * 3)
  596. roundtrip(['', '', ''], [set([''])] * 3)
  597. roundtrip([set(['dq']), set(['a']), set(['5'])])
  598. roundtrip(['dq', 'a', '5'], [set(['dq']), set(['a']),
  599. set(['5'])])
  600. roundtrip([1, 1, 1], [set(['dq']), set(['a']), set(['5'
  601. ])])
  602. roundtrip([set(['dq', 'sq']), None, set(['9', '5', '7'
  603. ])])
  604. set_table.insert().execute({'s3': set(['5'])}, {'s3'
  605. : set(['5', '7'])}, {'s3': set(['5', '7', '9'])},
  606. {'s3': set(['7', '9'])})
  607. rows = select([set_table.c.s3], set_table.c.s3.in_([set(['5'
  608. ]), set(['5', '7']), set(['7', '5'
  609. ])])).execute().fetchall()
  610. found = set([frozenset(row[0]) for row in rows])
  611. eq_(found, set([frozenset(['5']), frozenset(['5', '7'])]))
  612. finally:
  613. meta.drop_all()
  614. @testing.uses_deprecated('Manually quoting ENUM value literals')
  615. def test_enum(self):
  616. """Exercise the ENUM type."""
  617. db = testing.db
  618. enum_table = Table('mysql_enum', MetaData(testing.db),
  619. Column('e1', mysql.ENUM("'a'", "'b'")),
  620. Column('e2', mysql.ENUM("'a'", "'b'"),
  621. nullable=False),
  622. Column('e2generic', Enum("a", "b"),
  623. nullable=False),
  624. Column('e3', mysql.ENUM("'a'", "'b'", strict=True)),
  625. Column('e4', mysql.ENUM("'a'", "'b'", strict=True),
  626. nullable=False),
  627. Column('e5', mysql.ENUM("a", "b")),
  628. Column('e5generic', Enum("a", "b")),
  629. Column('e6', mysql.ENUM("'a'", "b")),
  630. )
  631. eq_(colspec(enum_table.c.e1),
  632. "e1 ENUM('a','b')")
  633. eq_(colspec(enum_table.c.e2),
  634. "e2 ENUM('a','b') NOT NULL")
  635. eq_(colspec(enum_table.c.e2generic),
  636. "e2generic ENUM('a','b') NOT NULL")
  637. eq_(colspec(enum_table.c.e3),
  638. "e3 ENUM('a','b')")
  639. eq_(colspec(enum_table.c.e4),
  640. "e4 ENUM('a','b') NOT NULL")
  641. eq_(colspec(enum_table.c.e5),
  642. "e5 ENUM('a','b')")
  643. eq_(colspec(enum_table.c.e5generic),
  644. "e5generic ENUM('a','b')")
  645. eq_(colspec(enum_table.c.e6),
  646. "e6 ENUM('''a''','b')")
  647. enum_table.drop(checkfirst=True)
  648. enum_table.create()
  649. assert_raises(exc.DBAPIError, enum_table.insert().execute,
  650. e1=None, e2=None, e3=None, e4=None)
  651. assert_raises(exc.StatementError, enum_table.insert().execute,
  652. e1='c', e2='c', e2generic='c', e3='c',
  653. e4='c', e5='c', e5generic='c', e6='c')
  654. enum_table.insert().execute()
  655. enum_table.insert().execute(e1='a', e2='a', e2generic='a', e3='a',
  656. e4='a', e5='a', e5generic='a', e6="'a'")
  657. enum_table.insert().execute(e1='b', e2='b', e2generic='b', e3='b',
  658. e4='b', e5='b', e5generic='b', e6='b')
  659. res = enum_table.select().execute().fetchall()
  660. expected = [(None, 'a', 'a', None, 'a', None, None, None),
  661. ('a', 'a', 'a', 'a', 'a', 'a', 'a', "'a'"),
  662. ('b', 'b', 'b', 'b', 'b', 'b', 'b', 'b')]
  663. # This is known to fail with MySQLDB 1.2.2 beta versions
  664. # which return these as sets.Set(['a']), sets.Set(['b'])
  665. # (even on Pythons with __builtin__.set)
  666. if (testing.against('mysql+mysqldb') and
  667. testing.db.dialect.dbapi.version_info < (1, 2, 2, 'beta', 3) and
  668. testing.db.dialect.dbapi.version_info >= (1, 2, 2)):
  669. # these mysqldb seem to always uses 'sets', even on later pythons
  670. import sets
  671. def convert(value):
  672. if value is None:
  673. return value
  674. if value == '':
  675. return sets.Set([])
  676. else:
  677. return sets.Set([value])
  678. e = []
  679. for row in expected:
  680. e.append(tuple([convert(c) for c in row]))
  681. expected = e
  682. eq_(res, expected)
  683. enum_table.drop()
  684. def test_unicode_enum(self):
  685. unicode_engine = utf8_engine()
  686. metadata = MetaData(unicode_engine)
  687. t1 = Table('table', metadata,
  688. Column('id', Integer, primary_key=True),
  689. Column('value', Enum(u'réveillé', u'drôle', u'S’il')),
  690. Column('value2', mysql.ENUM(u'réveillé', u'drôle', u'S’il'))
  691. )
  692. metadata.create_all()
  693. try:
  694. t1.insert().execute(value=u'drôle', value2=u'drôle')
  695. t1.insert().execute(value=u'réveillé', value2=u'réveillé')
  696. t1.insert().execute(value=u'SÂ’il', value2=u'SÂ’il')
  697. eq_(t1.select().order_by(t1.c.id).execute().fetchall(),
  698. [(1, u'drôle', u'drôle'), (2, u'réveillé', u'réveillé'),
  699. (3, u'SÂ’il', u'SÂ’il')]
  700. )
  701. # test reflection of the enum labels
  702. m2 = MetaData(testing.db)
  703. t2 = Table('table', m2, autoload=True)
  704. # TODO: what's wrong with the last element ? is there
  705. # latin-1 stuff forcing its way in ?
  706. assert t2.c.value.type.enums[0:2] == \
  707. (u'réveillé', u'drôle') #, u'S’il') # eh ?
  708. assert t2.c.value2.type.enums[0:2] == \
  709. (u'réveillé', u'drôle') #, u'S’il') # eh ?
  710. finally:
  711. metadata.drop_all()
  712. def test_enum_compile(self):
  713. e1 = Enum('x', 'y', 'z', name='somename')
  714. t1 = Table('sometable', MetaData(), Column('somecolumn', e1))
  715. self.assert_compile(schema.CreateTable(t1),
  716. "CREATE TABLE sometable (somecolumn "
  717. "ENUM('x','y','z'))")
  718. t1 = Table('sometable', MetaData(), Column('somecolumn',
  719. Enum('x', 'y', 'z', native_enum=False)))
  720. self.assert_compile(schema.CreateTable(t1),
  721. "CREATE TABLE sometable (somecolumn "
  722. "VARCHAR(1), CHECK (somecolumn IN ('x', "
  723. "'y', 'z')))")
  724. @testing.exclude('mysql', '<', (4,), "3.23 can't handle an ENUM of ''")
  725. @testing.uses_deprecated('Manually quoting ENUM value literals')
  726. def test_enum_parse(self):
  727. """More exercises for the ENUM type."""
  728. # MySQL 3.23 can't handle an ENUM of ''....
  729. enum_table = Table('mysql_enum', MetaData(testing.db),
  730. Column('e1', mysql.ENUM("'a'")),
  731. Column('e2', mysql.ENUM("''")),
  732. Column('e3', mysql.ENUM('a')),
  733. Column('e4', mysql.ENUM('')),
  734. Column('e5', mysql.ENUM("'a'", "''")),
  735. Column('e6', mysql.ENUM("''", "'a'")),
  736. Column('e7', mysql.ENUM("''", "'''a'''", "'b''b'", "''''")))
  737. for col in enum_table.c:
  738. self.assert_(repr(col))
  739. try:
  740. enum_table.create()
  741. reflected = Table('mysql_enum', MetaData(testing.db),
  742. autoload=True)
  743. for t in enum_table, reflected:
  744. eq_(t.c.e1.type.enums, ("a",))
  745. eq_(t.c.e2.type.enums, ("",))
  746. eq_(t.c.e3.type.enums, ("a",))
  747. eq_(t.c.e4.type.enums, ("",))
  748. eq_(t.c.e5.type.enums, ("a", ""))
  749. eq_(t.c.e6.type.enums, ("", "a"))
  750. eq_(t.c.e7.type.enums, ("", "'a'", "b'b", "'"))
  751. finally:
  752. enum_table.drop()
  753. class ReflectionTest(fixtures.TestBase, AssertsExecutionResults):
  754. __only_on__ = 'mysql'
  755. def test_default_reflection(self):
  756. """Test reflection of column defaults."""
  757. from sqlalchemy.dialects.mysql import VARCHAR
  758. def_table = Table(
  759. 'mysql_def',
  760. MetaData(testing.db),
  761. Column('c1', VARCHAR(10, collation='utf8_unicode_ci'),
  762. DefaultClause(''), nullable=False),
  763. Column('c2', String(10), DefaultClause('0')),
  764. Column('c3', String(10), DefaultClause('abc')),
  765. Column('c4', TIMESTAMP, DefaultClause('2009-04-05 12:00:00'
  766. )),
  767. Column('c5', TIMESTAMP),
  768. Column('c6', TIMESTAMP,
  769. DefaultClause(sql.text("CURRENT_TIMESTAMP "
  770. "ON UPDATE CURRENT_TIMESTAMP"))),
  771. )
  772. def_table.create()
  773. try:
  774. reflected = Table('mysql_def', MetaData(testing.db),
  775. autoload=True)
  776. finally:
  777. def_table.drop()
  778. assert def_table.c.c1.server_default.arg == ''
  779. assert def_table.c.c2.server_default.arg == '0'
  780. assert def_table.c.c3.server_default.arg == 'abc'
  781. assert def_table.c.c4.server_default.arg \
  782. == '2009-04-05 12:00:00'
  783. assert str(reflected.c.c1.server_default.arg) == "''"
  784. assert str(reflected.c.c2.server_default.arg) == "'0'"
  785. assert str(reflected.c.c3.server_default.arg) == "'abc'"
  786. assert str(reflected.c.c4.server_default.arg) \
  787. == "'2009-04-05 12:00:00'"
  788. assert reflected.c.c5.default is None
  789. assert reflected.c.c5.server_default is None
  790. assert reflected.c.c6.default is None
  791. eq_(
  792. str(reflected.c.c6.server_default.arg).upper(),
  793. "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"
  794. )
  795. reflected.create()
  796. try:
  797. reflected2 = Table('mysql_def', MetaData(testing.db),
  798. autoload=True)
  799. finally:
  800. reflected.drop()
  801. assert str(reflected2.c.c1.server_default.arg) == "''"
  802. assert str(reflected2.c.c2.server_default.arg) == "'0'"
  803. assert str(reflected2.c.c3.server_default.arg) == "'abc'"
  804. assert str(reflected2.c.c4.server_default.arg) \
  805. == "'2009-04-05 12:00:00'"
  806. assert reflected.c.c5.default is None
  807. assert reflected.c.c5.server_default is None
  808. assert reflected.c.c6.default is None
  809. eq_(
  810. str(reflected.c.c6.server_default.arg).upper(),
  811. "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"
  812. )
  813. def test_reflection_with_table_options(self):
  814. comment = r"""Comment types type speedily ' " \ '' Fun!"""
  815. def_table = Table('mysql_def', MetaData(testing.db),
  816. Column('c1', Integer()),
  817. mysql_engine='MEMORY',
  818. mysql_comment=comment,
  819. mysql_default_charset='utf8',
  820. mysql_auto_increment='5',
  821. mysql_avg_row_length='3',
  822. mysql_password='secret',
  823. mysql_connection='fish',
  824. )
  825. def_table.create()
  826. try:
  827. reflected = Table('mysql_def', MetaData(testing.db),
  828. autoload=True)
  829. finally:
  830. def_table.drop()
  831. assert def_table.kwargs['mysql_engine'] == 'MEMORY'
  832. assert def_table.kwargs['mysql_comment'] == comment
  833. assert def_table.kwargs['mysql_default_charset'] == 'utf8'
  834. assert def_table.kwargs['mysql_auto_increment'] == '5'
  835. assert def_table.kwargs['mysql_avg_row_length'] == '3'
  836. assert def_table.kwargs['mysql_password'] == 'secret'
  837. assert def_table.kwargs['mysql_connection'] == 'fish'
  838. assert reflected.kwargs['mysql_engine'] == 'MEMORY'
  839. assert reflected.kwargs['mysql_comment'] == comment
  840. assert reflected.kwargs['mysql_default charset'] == 'utf8'
  841. assert reflected.kwargs['mysql_avg_row_length'] == '3'
  842. assert reflected.kwargs['mysql_connection'] == 'fish'
  843. # This field doesn't seem to be returned by mysql itself.
  844. #assert reflected.kwargs['mysql_password'] == 'secret'
  845. # This is explicitly ignored when reflecting schema.
  846. #assert reflected.kwargs['mysql_auto_increment'] == '5'
  847. def test_reflection_on_include_columns(self):
  848. """Test reflection of include_columns to be sure they respect case."""
  849. case_table = Table('mysql_case', MetaData(testing.db),
  850. Column('c1', String(10)),
  851. Column('C2', String(10)),
  852. Column('C3', String(10)))
  853. try:
  854. case_table.create()
  855. reflected = Table('mysql_case', MetaData(testing.db),
  856. autoload=True, include_columns=['c1', 'C2'])
  857. for t in case_table, reflected:
  858. assert 'c1' in t.c.keys()
  859. assert 'C2' in t.c.keys()
  860. reflected2 = Table('mysql_case', MetaData(testing.db),
  861. autoload=True, include_columns=['c1', 'c2'])
  862. assert 'c1' in reflected2.c.keys()
  863. for c in ['c2', 'C2', 'C3']:
  864. assert c not in reflected2.c.keys()
  865. finally:
  866. case_table.drop()
  867. @testing.exclude('mysql', '<', (5, 0, 0), 'early types are squirrely')
  868. @testing.uses_deprecated('Using String type with no length')
  869. @testing.uses_deprecated('Manually quoting ENUM value literals')
  870. def test_type_reflection(self):
  871. # (ask_for, roundtripped_as_if_different)
  872. specs = [( String(1), mysql.MSString(1), ),
  873. ( String(3), mysql.MSString(3), ),
  874. ( Text(), mysql.MSText(), ),
  875. ( Unicode(1), mysql.MSString(1), ),
  876. ( Unicode(3), mysql.MSString(3), ),
  877. ( UnicodeText(), mysql.MSText(), ),
  878. ( mysql.MSChar(1), ),
  879. ( mysql.MSChar(3), ),
  880. ( NCHAR(2), mysql.MSChar(2), ),
  881. ( mysql.MSNChar(2), mysql.MSChar(2), ), # N is CREATE only
  882. ( mysql.MSNVarChar(22), mysql.MSString(22), ),
  883. ( SmallInteger(), mysql.MSSmallInteger(), ),
  884. ( SmallInteger(), mysql.MSSmallInteger(4), ),
  885. ( mysql.MSSmallInteger(), ),
  886. ( mysql.MSSmallInteger(4), mysql.MSSmallInteger(4), ),
  887. ( mysql.MSMediumInteger(), mysql.MSMediumInteger(), ),
  888. ( mysql.MSMediumInteger(8), mysql.MSMediumInteger(8), ),
  889. ( LargeBinary(3), mysql.TINYBLOB(), ),
  890. ( LargeBinary(), mysql.BLOB() ),
  891. ( mysql.MSBinary(3), mysql.MSBinary(3), ),
  892. ( mysql.MSVarBinary(3),),
  893. ( mysql.MSTinyBlob(),),
  894. ( mysql.MSBlob(),),
  895. ( mysql.MSBlob(1234), mysql.MSBlob()),
  896. ( mysql.MSMediumBlob(),),
  897. ( mysql.MSLongBlob(),),
  898. ( mysql.ENUM("''","'fleem'"), ),
  899. ]
  900. columns = [Column('c%i' % (i + 1), t[0]) for i, t in enumerate(specs)]
  901. db = testing.db
  902. m = MetaData(db)
  903. t_table = Table('mysql_types', m, *columns)
  904. try:
  905. m.create_all()
  906. m2 = MetaData(db)
  907. rt = Table('mysql_types', m2, autoload=True)
  908. try:
  909. db.execute('CREATE OR REPLACE VIEW mysql_types_v '
  910. 'AS SELECT * from mysql_types')
  911. rv = Table('mysql_types_v', m2, autoload=True)
  912. expected = [len(c) > 1 and c[1] or c[0] for c in specs]
  913. # Early 5.0 releases seem to report more "general" for columns
  914. # in a view, e.g. char -> varchar, tinyblob -> mediumblob
  915. #
  916. # Not sure exactly which point version has the fix.
  917. if db.dialect.server_version_info < (5, 0, 11):
  918. tables = rt,
  919. else:
  920. tables = rt, rv
  921. for table in tables:
  922. for i, reflected in enumerate(table.c):
  923. assert isinstance(reflected.type,
  924. type(expected[i])), \
  925. 'element %d: %r not instance of %r' % (i,
  926. reflected.type, type(expected[i]))
  927. finally:
  928. db.execute('DROP VIEW mysql_types_v')
  929. finally:
  930. m.drop_all()
  931. def test_autoincrement(self):
  932. meta = MetaData(testing.db)
  933. try:
  934. Table('ai_1', meta,
  935. Column('int_y', Integer, primary_key=True),
  936. Column('int_n', Integer, DefaultClause('0'),
  937. primary_key=True),
  938. mysql_engine='MyISAM')
  939. Table('ai_2', meta,
  940. Column('int_y', Integer, primary_key=True),
  941. Column('int_n', Integer, DefaultClause('0'),
  942. primary_key=True),
  943. mysql_engine='MyISAM')
  944. Table('ai_3', meta,
  945. Column('int_n', Integer, DefaultClause('0'),
  946. primary_key=True, autoincrement=False),
  947. Column('int_y', Integer, primary_key=True),
  948. mysql_engine='MyISAM')
  949. Table('ai_4', meta,
  950. Column('int_n', Integer, DefaultClause('0'),
  951. primary_key=True, autoincrement=False),
  952. Column('int_n2', Integer, DefaultClause('0'),
  953. primary_key=True, autoincrement=False),
  954. mysql_engine='MyISAM')
  955. Table('ai_5', meta,
  956. Column('int_y', Integer, primary_key=True),
  957. Column('int_n', Integer, DefaultClause('0'),
  958. primary_key=True, autoincrement=False),
  959. mysql_engine='MyISAM')
  960. Table('ai_6', meta,
  961. Column('o1', String(1), DefaultClause('x'),
  962. primary_key=True),
  963. Column('int_y', Integer, primary_key=True),
  964. mysql_engine='MyISAM')
  965. Table('ai_7', meta,
  966. Column('o1', String(1), DefaultClause('x'),
  967. primary_key=True),
  968. Column('o2', String(1), DefaultClause('x'),
  969. primary_key=True),
  970. Column('int_y', Integer, primary_key=True),
  971. mysql_engine='MyISAM')
  972. Table('ai_8', meta,
  973. Column('o1', String(1), DefaultClause('x'),
  974. primary_key=True),
  975. Column('o2', String(1), DefaultClause('x'),
  976. primary_key=True),
  977. mysql_engine='MyISAM')
  978. meta.create_all()
  979. table_names = ['ai_1', 'ai_2', 'ai_3', 'ai_4',
  980. 'ai_5', 'ai_6', 'ai_7', 'ai_8']
  981. mr = MetaData(testing.db)
  982. mr.reflect(only=table_names)
  983. for tbl in [mr.tables[name] for name in table_names]:
  984. for c in tbl.c:
  985. if c.name.startswith('int_y'):
  986. assert c.autoincrement
  987. elif c.name.startswith('int_n'):
  988. assert not c.autoincrement
  989. tbl.insert().execute()
  990. if 'int_y' in tbl.c:
  991. assert select([tbl.c.int_y]).scalar() == 1
  992. assert list(tbl.select().execute().first()).count(1) == 1
  993. else:
  994. assert 1 not in list(tbl.select().execute().first())
  995. finally:
  996. meta.drop_all()
  997. @testing.exclude('mysql', '<', (5, 0, 0), 'no information_schema support')
  998. def test_system_views(self):
  999. dialect = testing.db.dialect
  1000. connection = testing.db.connect()
  1001. view_names = dialect.get_view_names(connection, "information_schema")
  1002. self.assert_('TABLES' in view_names)
  1003. class SQLTest(fixtures.TestBase, AssertsCompiledSQL):
  1004. """Tests MySQL-dialect specific compilation."""
  1005. __dialect__ = mysql.dialect()
  1006. def test_precolumns(self):
  1007. dialect = self.__dialect__
  1008. def gen(distinct=None, prefixes=None):
  1009. kw = {}
  1010. if distinct is not None:
  1011. kw['distinct'] = distinct
  1012. if prefixes is not None:
  1013. kw['prefixes'] = prefixes
  1014. return str(select(['q'], **kw).compile(dialect=dialect))
  1015. eq_(gen(None), 'SELECT q')
  1016. eq_(gen(True), 'SELECT DISTINCT q')
  1017. assert_raises(
  1018. exc.SADeprecationWarning,
  1019. gen, 'DISTINCT'
  1020. )
  1021. eq_(gen(prefixes=['ALL']), 'SELECT ALL q')
  1022. eq_(gen(prefixes=['DISTINCTROW']),
  1023. 'SELECT DISTINCTROW q')
  1024. # Interaction with MySQL prefix extensions
  1025. eq_(
  1026. gen(None, ['straight_join']),
  1027. 'SELECT straight_join q')
  1028. eq_(
  1029. gen(False, ['HIGH_PRIORITY', 'SQL_SMALL_RESULT', 'ALL']),
  1030. 'SELECT HIGH_PRIORITY SQL_SMALL_RESULT ALL q')
  1031. eq_(
  1032. gen(True, ['high_priority', sql.text('sql_cache')]),
  1033. 'SELECT high_priority sql_cache DISTINCT q')
  1034. @testing.uses_deprecated
  1035. def test_deprecated_distinct(self):
  1036. dialect = self.__dialect__
  1037. self.assert_compile(
  1038. select(['q'], distinct='ALL'),
  1039. 'SELECT ALL q',
  1040. )
  1041. self.assert_compile(
  1042. select(['q'], distinct='distinctROW'),
  1043. 'SELECT DISTINCTROW q',
  1044. )
  1045. self.assert_compile(
  1046. select(['q'], distinct='ALL',
  1047. prefixes=['HIGH_PRIORITY', 'SQL_SMALL_RESULT']),
  1048. 'SELECT HIGH_PRIORITY SQL_SMALL_RESULT ALL q'
  1049. )
  1050. def test_backslash_escaping(self):
  1051. self.assert_compile(
  1052. sql.column('foo').like('bar', escape='\\'),
  1053. "foo LIKE %s ESCAPE '\\\\'"
  1054. )
  1055. dialect = mysql.dialect()
  1056. dialect._backslash_escapes=False
  1057. self.assert_compile(
  1058. sql.column('foo').like('bar', escape='\\'),
  1059. "foo LIKE %s ESCAPE '\\'",
  1060. dialect=dialect
  1061. )
  1062. def test_limit(self):
  1063. t = sql.table('t', sql.column('col1'), sql.column('col2'))
  1064. self.assert_compile(
  1065. select([t]).limit(10).offset(20),
  1066. "SELECT t.col1, t.col2 FROM t LIMIT %s, %s",
  1067. {'param_1':20, 'param_2':10}
  1068. )
  1069. self.assert_compile(
  1070. select([t]).limit(10),
  1071. "SELECT t.col1, t.col2 FROM t LIMIT %s",
  1072. {'param_1':10})
  1073. self.assert_compile(
  1074. select([t]).offset(10),
  1075. "SELECT t.col1, t.col2 FROM t LIMIT %s, 18446744073709551615",
  1076. {'param_1':10}
  1077. )
  1078. def test_varchar_raise(self):
  1079. for type_ in (
  1080. String,
  1081. VARCHAR,
  1082. String(),
  1083. VARCHAR(),
  1084. NVARCHAR(),
  1085. Unicode,
  1086. Unicode(),
  1087. ):
  1088. type_ = sqltypes.to_instance(type_)
  1089. assert_raises_message(
  1090. exc.CompileError,
  1091. "VARCHAR requires a length on dialect mysql",
  1092. type_.compile,
  1093. dialect=mysql.dialect())
  1094. t1 = Table('sometable', MetaData(),
  1095. Column('somecolumn', type_)
  1096. )
  1097. assert_raises_message(
  1098. exc.CompileError,
  1099. r"\(in table 'sometable', column 'somecolumn'\)\: "
  1100. r"(?:N)?VARCHAR requires a length on dialect mysql",
  1101. schema.CreateTable(t1).compile,
  1102. dialect=mysql.dialect()
  1103. )
  1104. def test_update_limit(self):
  1105. t = sql.table('t', sql.column('col1'), sql.column('col2'))
  1106. self.assert_compile(
  1107. t.update(values={'col1':123}),
  1108. "UPDATE t SET col1=%s"
  1109. )
  1110. self.assert_compile(
  1111. t.update(values={'col1':123}, mysql_limit=5),
  1112. "UPDATE t SET col1=%s LIMIT 5"
  1113. )
  1114. self.assert_compile(
  1115. t.update(values={'col1':123}, mysql_limit=None),
  1116. "UPDATE t SET col1=%s"
  1117. )
  1118. self.assert_compile(
  1119. t.update(t.c.col2==456, values={'col1':123}, mysql_limit=1),
  1120. "UPDATE t SET col1=%s WHERE t.col2 = %s LIMIT 1"
  1121. )
  1122. def test_utc_timestamp(self):
  1123. self.assert_compile(func.utc_timestamp(), "UTC_TIMESTAMP")
  1124. def test_sysdate(self):
  1125. self.assert_compile(func.sysdate(), "SYSDATE()")
  1126. def test_cast(self):
  1127. t = sql.table('t', sql.column('col'))
  1128. m = mysql
  1129. specs = [
  1130. (Integer, "CAST(t.col AS SIGNED INTEGER)"),
  1131. (INT, "CAST(t.col AS SIGNED INTEGER)"),
  1132. (m.MSInteger, "CAST(t.col AS SIGNED INTEGER)"),
  1133. (m.MSInteger(unsigned=True), "CAST(t.col AS UNSIGNED INTEGER)"),
  1134. (SmallInteger, "CAST(t.col AS SIGNED INTEGER)"),
  1135. (m.MSSmallInteger, "CAST(t.col AS SIGNED INTEGER)"),
  1136. (m.MSTinyInteger, "CAST(t.col AS SIGNED INTEGER)"),
  1137. # 'SIGNED INTEGER' is a bigint, so this is ok.
  1138. (m.MSBigInteger, "CAST(t.col AS SIGNED INTEGER)"),
  1139. (m.MSBigInteger(unsigned=False), "CAST(t.col AS SIGNED INTEGER)"),
  1140. (m.MSBigInteger(unsigned=True),
  1141. "CAST(t.col AS UNSIGNED INTEGER)"),
  1142. (m.MSBit, "t.col"),
  1143. # this is kind of sucky. thank you default arguments!
  1144. (NUMERIC, "CAST(t.col AS DECIMAL)"),
  1145. (DECIMAL, "CAST(t.col AS DECIMAL)"),
  1146. (Numeric, "CAST(t.col AS DECIMAL)"),
  1147. (m.MSNumeric, "CAST(t.col AS DECIMAL)"),
  1148. (m.MSDecimal, "CAST(t.col AS DECIMAL)"),
  1149. (FLOAT, "t.col"),
  1150. (Float, "t.col"),
  1151. (m.MSFloat, "t.col"),
  1152. (m.MSDouble, "t.col"),
  1153. (m.MSReal, "t.col"),
  1154. (TIMESTAMP, "CAST(t.col AS DATETIME)"),
  1155. (DATETIME, "CAST(t.col AS DATETIME)"),
  1156. (DATE, "CAST(t.col AS DATE)"),
  1157. (TIME, "CAST(t.col AS TIME)"),
  1158. (DateTime, "CAST(t.col AS DATETIME)"),
  1159. (Date, "CAST(t.col AS DATE)"),
  1160. (Time, "CAST(t.col AS TIME)"),
  1161. (DateTime, "CAST(t.col AS DATETIME)"),
  1162. (Date, "CAST(t.col AS DATE)"),
  1163. (m.MSTime, "CAST(t.col AS TIME)"),
  1164. (m.MSTimeStamp, "CAST(t.col AS DATETIME)"),
  1165. (m.MSYear, "t.col"),
  1166. (m.MSYear(2), "t.col"),
  1167. (Interval, "t.col"),
  1168. (String, "CAST(t.col AS CHAR)"),
  1169. (Unicode, "CAST(t.col AS CHAR)"),
  1170. (UnicodeText, "CAST(t.col AS CHAR)"),
  1171. (VARCHAR, "CAST(t.col AS CHAR)"),
  1172. (NCHAR, "CAST(t.col AS CHAR)"),
  1173. (CHAR, "CAST(t.col AS CHAR)"),
  1174. (CLOB, "CAST(t.col AS CHAR)"),
  1175. (TEXT, "CAST(t.col AS CHAR)"),
  1176. (String(32), "CAST(t.col AS CHAR(32))"),
  1177. (Unicode(32), "CAST(t.col AS CHAR(32))"),
  1178. (CHAR(32), "CAST(t.col AS CHAR(32))"),
  1179. (m.MSString, "CAST(t.col AS CHAR)"),
  1180. (m.MSText, "CAST(t.col AS CHAR)"),
  1181. (m.MSTinyText, "CAST(t.col AS CHAR)"),
  1182. (m.MSMediumText, "CAST(t.col AS CHAR)"),
  1183. (m.MSLongText, "CAST(t.col AS CHAR)"),
  1184. (m.MSNChar, "CAST(t.col AS CHAR)"),
  1185. (m.MSNVarChar, "CAST(t.col AS CHAR)"),
  1186. (LargeBinary, "CAST(t.col AS BINARY)"),
  1187. (BLOB, "CAST(t.col AS BINARY)"),
  1188. (m.MSBlob, "CAST(t.col AS BINARY)"),
  1189. (m.MSBlob(32), "CAST(t.col AS BINARY)"),
  1190. (m.MSTinyBlob, "CAST(t.col AS BINARY)"),
  1191. (m.MSMediumBlob, "CAST(t.col AS BINARY)"),
  1192. (m.MSLongBlob, "CAST(t.col AS BINARY)"),
  1193. (m.MSBinary, "CAST(t.col AS BINARY)"),
  1194. (m.MSBinary(32), "CAST(t.col AS BINARY)"),
  1195. (m.MSVarBinary, "CAST(t.col AS BINARY)"),
  1196. (m.MSVarBinary(32), "CAST(t.col AS BINARY)"),
  1197. # maybe this could be changed to something more DWIM, needs
  1198. # testing
  1199. (Boolean, "t.col"),
  1200. (BOOLEAN, "t.col"),
  1201. (m.MSEnum, "t.col"),
  1202. (m.MSEnum("1", "2"), "t.col"),
  1203. (m.MSSet, "t.col"),
  1204. (m.MSSet("1", "2"), "t.col"),
  1205. ]
  1206. for type_, expected in specs:
  1207. self.assert_compile(cast(t.c.col, type_), expected)
  1208. def test_no_cast_pre_4(self):
  1209. self.assert_compile(
  1210. cast(Column('foo', Integer), String),
  1211. "CAST(foo AS CHAR)",
  1212. )
  1213. dialect = mysql.dialect()
  1214. dialect.server_version_info = (3, 2, 3)
  1215. self.assert_compile(
  1216. cast(Column('foo', Integer), String),
  1217. "foo",
  1218. dialect=dialect
  1219. )
  1220. def test_cast_grouped_expression_non_castable(self):
  1221. self.assert_compile(
  1222. cast(sql.column('x') + sql.column('y'), Float),
  1223. "(x + y)"
  1224. )
  1225. def test_cast_grouped_expression_pre_4(self):
  1226. dialect = mysql.dialect()
  1227. dialect.server_version_info = (3, 2, 3)
  1228. self.assert_compile(
  1229. cast(sql.column('x') + sql.column('y'), Integer),
  1230. "(x + y)",
  1231. dialect=dialect
  1232. )
  1233. def test_extract(self):
  1234. t = sql.table('t', sql.column('col1'))
  1235. for field in 'year', 'month', 'day':
  1236. self.assert_compile(
  1237. select([extract(field, t.c.col1)]),
  1238. "SELECT EXTRACT(%s FROM t.col1) AS anon_1 FROM t" % field)
  1239. # millsecondS to millisecond
  1240. self.assert_compile(
  1241. select([extract('milliseconds', t.c.col1)]),
  1242. "SELECT EXTRACT(millisecond FROM t.col1) AS anon_1 FROM t")
  1243. def test_too_long_index(self):
  1244. exp = 'ix_zyrenian_zyme_zyzzogeton_zyzzogeton_zyrenian_zyme_zyz_5cd2'
  1245. tname = 'zyrenian_zyme_zyzzogeton_zyzzogeton'
  1246. cname = 'zyrenian_zyme_zyzzogeton_zo'
  1247. t1 = Table(tname, MetaData(),
  1248. Column(cname, Integer, index=True),
  1249. )
  1250. ix1 = list(t1.indexes)[0]
  1251. self.assert_compile(
  1252. schema.CreateIndex(ix1),
  1253. "CREATE INDEX %s "
  1254. "ON %s (%s)" % (exp, tname, cname),
  1255. dialect=mysql.dialect()
  1256. )
  1257. def test_innodb_autoincrement(self):
  1258. t1 = Table('sometable', MetaData(), Column('assigned_id',
  1259. Integer(), primary_key=True, autoincrement=False),
  1260. Column('id', Integer(), primary_key=True,
  1261. autoincrement=True), mysql_engine='InnoDB')
  1262. self.assert_compile(schema.CreateTable(t1),
  1263. 'CREATE TABLE sometable (assigned_id '
  1264. 'INTEGER NOT NULL, id INTEGER NOT NULL '
  1265. 'AUTO_INCREMENT, PRIMARY KEY (assigned_id, '
  1266. 'id), KEY idx_autoinc_id (id))ENGINE=Inn'
  1267. 'oDB')
  1268. t1 = Table('sometable', MetaData(), Column('assigned_id',
  1269. Integer(), primary_key=True, autoincrement=True),
  1270. Column('id', Integer(), primary_key=True,
  1271. autoincrement=False), mysql_engine='InnoDB')
  1272. self.assert_compile(schema.CreateTable(t1),
  1273. 'CREATE TABLE sometable (assigned_id '
  1274. 'INTEGER NOT NULL AUTO_INCREMENT, id '
  1275. 'INTEGER NOT NULL, PRIMARY KEY '
  1276. '(assigned_id, id))ENGINE=InnoDB')
  1277. def test_innodb_autoincrement_reserved_word_column_name(self):
  1278. t1 = Table(
  1279. 'sometable', MetaData(),
  1280. Column('id', Integer(), primary_key=True, autoincrement=False),
  1281. Column('order', Integer(), primary_key=True, autoincrement=True),
  1282. mysql_engine='InnoDB')
  1283. self.assert_compile(
  1284. schema.CreateTable(t1),
  1285. 'CREATE TABLE sometable ('
  1286. 'id INTEGER NOT NULL, '
  1287. '`order` INTEGER NOT NULL AUTO_INCREMENT, '
  1288. 'PRIMARY KEY (id, `order`), '
  1289. 'KEY idx_autoinc_order (`order`)'
  1290. ')ENGINE=InnoDB')
  1291. class SQLModeDetectionTest(fixtures.TestBase):
  1292. __only_on__ = 'mysql'
  1293. def _options(self, modes):
  1294. def connect(con, record):
  1295. cursor = con.cursor()
  1296. print "DOING THiS:", "set sql_mode='%s'" % (",".join(modes))
  1297. cursor.execute("set sql_mode='%s'" % (",".join(modes)))
  1298. e = engines.testing_engine(options={
  1299. 'pool_events':[
  1300. (connect, 'first_connect'),
  1301. (connect, 'connect')
  1302. ]
  1303. })
  1304. return e
  1305. def test_backslash_escapes(self):
  1306. engine = self._options(['NO_BACKSLASH_ESCAPES'])
  1307. c = engine.connect()
  1308. assert not engine.dialect._backslash_escapes
  1309. c.close()
  1310. engine.dispose()
  1311. engine = self._options([])
  1312. c = engine.connect()
  1313. assert engine.dialect._backslash_escapes
  1314. c.close()
  1315. engine.dispose()
  1316. def test_ansi_quotes(self):
  1317. engine = self._options(['ANSI_QUOTES'])
  1318. c = engine.connect()
  1319. assert engine.dialect._server_ansiquotes
  1320. c.close()
  1321. engine.dispose()
  1322. def test_combination(self):
  1323. engine = self._options(['ANSI_QUOTES,NO_BACKSLASH_ESCAPES'])
  1324. c = engine.connect()
  1325. assert engine.dialect._server_ansiquotes
  1326. assert not engine.dialect._backslash_escapes
  1327. c.close()
  1328. engine.dispose()
  1329. class RawReflectionTest(fixtures.TestBase):
  1330. def setup(self):
  1331. dialect = mysql.dialect()
  1332. self.parser = mysql.MySQLTableDefinitionParser(dialect, dialect.identifier_preparer)
  1333. def test_key_reflection(self):
  1334. regex = self.parser._re_key
  1335. assert regex.match(' PRIMARY KEY (`id`),')
  1336. assert regex.match(' PRIMARY KEY USING BTREE (`id`),')
  1337. assert regex.match(' PRIMARY KEY (`id`) USING BTREE,')
  1338. assert regex.match(' PRIMARY KEY (`id`)')
  1339. assert regex.match(' PRIMARY KEY USING BTREE (`id`)')
  1340. assert regex.match(' PRIMARY KEY (`id`) USING BTREE')
  1341. class ExecutionTest(fixtures.TestBase):
  1342. """Various MySQL execution special cases."""
  1343. __only_on__ = 'mysql'
  1344. def test_charset_caching(self):
  1345. engine = engines.testing_engine()
  1346. cx = engine.connect()
  1347. meta = MetaData()
  1348. charset = engine.dialect._detect_charset(cx)
  1349. meta.reflect(cx)
  1350. eq_(cx.dialect._connection_charset, charset)
  1351. cx.close()
  1352. def test_sysdate(self):
  1353. d = testing.db.scalar(func.sysdate())
  1354. assert isinstance(d, datetime.datetime)
  1355. class MatchTest(fixtures.TestBase, AssertsCompiledSQL):
  1356. __only_on__ = 'mysql'
  1357. @classmethod
  1358. def setup_class(cls):
  1359. global metadata, cattable, matchtable
  1360. metadata = MetaData(testing.db)
  1361. cattable = Table('cattable', metadata,
  1362. Column('id', Integer, primary_key=True),
  1363. Column('description', String(50)),
  1364. mysql_engine='MyISAM'
  1365. )
  1366. matchtable = Table('matchtable', metadata,
  1367. Column('id', Integer, primary_key=True),
  1368. Column('title', String(200)),
  1369. Column('category_id', Integer, ForeignKey('cattable.id')),
  1370. mysql_engine='MyISAM'
  1371. )
  1372. metadata.create_all()
  1373. cattable.insert().execute([
  1374. {'id': 1, 'description': 'Python'},
  1375. {'id': 2, 'description': 'Ruby'},
  1376. ])
  1377. matchtable.insert().execute([
  1378. {'id': 1,
  1379. 'title': 'Agile Web Development with Rails',
  1380. 'category_id': 2},
  1381. {'id': 2,
  1382. 'title': 'Dive Into Python',
  1383. 'category_id': 1},
  1384. {'id': 3,
  1385. 'title': "Programming Matz's Ruby",
  1386. 'category_id': 2},
  1387. {'id': 4,
  1388. 'title': 'The Definitive Guide to Django',
  1389. 'category_id': 1},
  1390. {'id': 5,
  1391. 'title': 'Python in a Nutshell',
  1392. 'category_id': 1}
  1393. ])
  1394. @classmethod
  1395. def teardown_class(cls):
  1396. metadata.drop_all()
  1397. @testing.fails_on('mysql+mysqlconnector', 'uses pyformat')
  1398. def test_expression(self):
  1399. format = testing.db.dialect.paramstyle == 'format' and '%s' or '?'
  1400. self.assert_compile(
  1401. matchtable.c.title.match('somstr'),
  1402. "MATCH (matchtable.title) AGAINST (%s IN BOOLEAN MODE)" % format)
  1403. @testing.fails_on('mysql+mysqldb', 'uses format')
  1404. @testing.fails_on('mysql+pymysql', 'uses format')
  1405. @testing.fails_on('mysql+oursql', 'uses format')
  1406. @testing.fails_on('mysql+pyodbc', 'uses format')
  1407. @testing.fails_on('mysql+zxjdbc', 'uses format')
  1408. def test_expression(self):
  1409. format = '%(title_1)s'
  1410. self.assert_compile(
  1411. matchtable.c.title.match('somstr'),
  1412. "MATCH (matchtable.title) AGAINST (%s IN BOOLEAN MODE)" % format)
  1413. def test_simple_match(self):
  1414. results = (matchtable.select().
  1415. where(matchtable.c.title.match('python')).
  1416. order_by(matchtable.c.id).
  1417. execute().
  1418. fetchall())
  1419. eq_([2, 5], [r.id for r in results])
  1420. def test_simple_match_with_apostrophe(self):
  1421. results = (matchtable.select().
  1422. where(matchtable.c.title.match("Matz's")).
  1423. execute().
  1424. fetchall())
  1425. eq_([3], [r.id for r in results])
  1426. def test_or_match(self):
  1427. results1 = (matchtable.select().
  1428. where(or_(matchtable.c.title.match('nutshell'),
  1429. matchtable.c.title.match('ruby'))).
  1430. order_by(matchtable.c.id).
  1431. execute().
  1432. fetchall())
  1433. eq_([3, 5], [r.id for r in results1])
  1434. results2 = (matchtable.select().
  1435. where(matchtable.c.title.match('nutshell ruby')).
  1436. order_by(matchtable.c.id).
  1437. execute().
  1438. fetchall())
  1439. eq_([3, 5], [r.id for r in results2])
  1440. def test_and_match(self):
  1441. results1 = (matchtable.select().
  1442. where(and_(matchtable.c.title.match('python'),
  1443. matchtable.c.title.match('nutshell'))).
  1444. execute().
  1445. fetchall())
  1446. eq_([5], [r.id for r in results1])
  1447. results2 = (matchtable.select().
  1448. where(matchtable.c.title.match('+python +nutshell')).
  1449. execute().
  1450. fetchall())
  1451. eq_([5], [r.id for r in results2])
  1452. def test_match_across_joins(self):
  1453. results = (matchtable.select().
  1454. where(and_(cattable.c.id==matchtable.c.category_id,
  1455. or_(cattable.c.description.match('Ruby'),
  1456. matchtable.c.title.match('nutshell')))).
  1457. order_by(matchtable.c.id).
  1458. execute().
  1459. fetchall())
  1460. eq_([1, 3, 5], [r.id for r in results])
  1461. def colspec(c):
  1462. return testing.db.dialect.ddl_compiler(
  1463. testing.db.dialect, None).get_column_specification(c)