test_dialect.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. # testing/suite/test_dialect.py
  2. # Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: https://www.opensource.org/licenses/mit-license.php
  7. # mypy: ignore-errors
  8. import importlib
  9. from . import testing
  10. from .. import assert_raises
  11. from .. import config
  12. from .. import engines
  13. from .. import eq_
  14. from .. import fixtures
  15. from .. import is_not_none
  16. from .. import is_true
  17. from .. import ne_
  18. from .. import provide_metadata
  19. from ..assertions import expect_raises
  20. from ..assertions import expect_raises_message
  21. from ..config import requirements
  22. from ..provision import set_default_schema_on_connection
  23. from ..schema import Column
  24. from ..schema import Table
  25. from ... import bindparam
  26. from ... import dialects
  27. from ... import event
  28. from ... import exc
  29. from ... import Integer
  30. from ... import literal_column
  31. from ... import select
  32. from ... import String
  33. from ...sql.compiler import Compiled
  34. from ...util import inspect_getfullargspec
  35. class PingTest(fixtures.TestBase):
  36. __backend__ = True
  37. def test_do_ping(self):
  38. with testing.db.connect() as conn:
  39. is_true(
  40. testing.db.dialect.do_ping(conn.connection.dbapi_connection)
  41. )
  42. class ArgSignatureTest(fixtures.TestBase):
  43. """test that all visit_XYZ() in :class:`_sql.Compiler` subclasses have
  44. ``**kw``, for #8988.
  45. This test uses runtime code inspection. Does not need to be a
  46. ``__backend__`` test as it only needs to run once provided all target
  47. dialects have been imported.
  48. For third party dialects, the suite would be run with that third
  49. party as a "--dburi", which means its compiler classes will have been
  50. imported by the time this test runs.
  51. """
  52. def _all_subclasses(): # type: ignore # noqa
  53. for d in dialects.__all__:
  54. if not d.startswith("_"):
  55. importlib.import_module("sqlalchemy.dialects.%s" % d)
  56. stack = [Compiled]
  57. while stack:
  58. cls = stack.pop(0)
  59. stack.extend(cls.__subclasses__())
  60. yield cls
  61. @testing.fixture(params=list(_all_subclasses()))
  62. def all_subclasses(self, request):
  63. yield request.param
  64. def test_all_visit_methods_accept_kw(self, all_subclasses):
  65. cls = all_subclasses
  66. for k in cls.__dict__:
  67. if k.startswith("visit_"):
  68. meth = getattr(cls, k)
  69. insp = inspect_getfullargspec(meth)
  70. is_not_none(
  71. insp.varkw,
  72. f"Compiler visit method {cls.__name__}.{k}() does "
  73. "not accommodate for **kw in its argument signature",
  74. )
  75. class ExceptionTest(fixtures.TablesTest):
  76. """Test basic exception wrapping.
  77. DBAPIs vary a lot in exception behavior so to actually anticipate
  78. specific exceptions from real round trips, we need to be conservative.
  79. """
  80. run_deletes = "each"
  81. __backend__ = True
  82. @classmethod
  83. def define_tables(cls, metadata):
  84. Table(
  85. "manual_pk",
  86. metadata,
  87. Column("id", Integer, primary_key=True, autoincrement=False),
  88. Column("data", String(50)),
  89. )
  90. @requirements.duplicate_key_raises_integrity_error
  91. def test_integrity_error(self):
  92. with config.db.connect() as conn:
  93. trans = conn.begin()
  94. conn.execute(
  95. self.tables.manual_pk.insert(), {"id": 1, "data": "d1"}
  96. )
  97. assert_raises(
  98. exc.IntegrityError,
  99. conn.execute,
  100. self.tables.manual_pk.insert(),
  101. {"id": 1, "data": "d1"},
  102. )
  103. trans.rollback()
  104. def test_exception_with_non_ascii(self):
  105. with config.db.connect() as conn:
  106. try:
  107. # try to create an error message that likely has non-ascii
  108. # characters in the DBAPI's message string. unfortunately
  109. # there's no way to make this happen with some drivers like
  110. # mysqlclient, pymysql. this at least does produce a non-
  111. # ascii error message for cx_oracle, psycopg2
  112. conn.execute(select(literal_column("méil")))
  113. assert False
  114. except exc.DBAPIError as err:
  115. err_str = str(err)
  116. assert str(err.orig) in str(err)
  117. assert isinstance(err_str, str)
  118. class IsolationLevelTest(fixtures.TestBase):
  119. __backend__ = True
  120. __requires__ = ("isolation_level",)
  121. def _get_non_default_isolation_level(self):
  122. levels = requirements.get_isolation_levels(config)
  123. default = levels["default"]
  124. supported = levels["supported"]
  125. s = set(supported).difference(["AUTOCOMMIT", default])
  126. if s:
  127. return s.pop()
  128. else:
  129. config.skip_test("no non-default isolation level available")
  130. def test_default_isolation_level(self):
  131. eq_(
  132. config.db.dialect.default_isolation_level,
  133. requirements.get_isolation_levels(config)["default"],
  134. )
  135. def test_non_default_isolation_level(self):
  136. non_default = self._get_non_default_isolation_level()
  137. with config.db.connect() as conn:
  138. existing = conn.get_isolation_level()
  139. ne_(existing, non_default)
  140. conn.execution_options(isolation_level=non_default)
  141. eq_(conn.get_isolation_level(), non_default)
  142. conn.dialect.reset_isolation_level(
  143. conn.connection.dbapi_connection
  144. )
  145. eq_(conn.get_isolation_level(), existing)
  146. def test_all_levels(self):
  147. levels = requirements.get_isolation_levels(config)
  148. all_levels = levels["supported"]
  149. for level in set(all_levels).difference(["AUTOCOMMIT"]):
  150. with config.db.connect() as conn:
  151. conn.execution_options(isolation_level=level)
  152. eq_(conn.get_isolation_level(), level)
  153. trans = conn.begin()
  154. trans.rollback()
  155. eq_(conn.get_isolation_level(), level)
  156. with config.db.connect() as conn:
  157. eq_(
  158. conn.get_isolation_level(),
  159. levels["default"],
  160. )
  161. @testing.requires.get_isolation_level_values
  162. def test_invalid_level_execution_option(self, connection_no_trans):
  163. """test for the new get_isolation_level_values() method"""
  164. connection = connection_no_trans
  165. with expect_raises_message(
  166. exc.ArgumentError,
  167. "Invalid value '%s' for isolation_level. "
  168. "Valid isolation levels for '%s' are %s"
  169. % (
  170. "FOO",
  171. connection.dialect.name,
  172. ", ".join(
  173. requirements.get_isolation_levels(config)["supported"]
  174. ),
  175. ),
  176. ):
  177. connection.execution_options(isolation_level="FOO")
  178. @testing.requires.get_isolation_level_values
  179. @testing.requires.dialect_level_isolation_level_param
  180. def test_invalid_level_engine_param(self, testing_engine):
  181. """test for the new get_isolation_level_values() method
  182. and support for the dialect-level 'isolation_level' parameter.
  183. """
  184. eng = testing_engine(options=dict(isolation_level="FOO"))
  185. with expect_raises_message(
  186. exc.ArgumentError,
  187. "Invalid value '%s' for isolation_level. "
  188. "Valid isolation levels for '%s' are %s"
  189. % (
  190. "FOO",
  191. eng.dialect.name,
  192. ", ".join(
  193. requirements.get_isolation_levels(config)["supported"]
  194. ),
  195. ),
  196. ):
  197. eng.connect()
  198. @testing.requires.independent_readonly_connections
  199. def test_dialect_user_setting_is_restored(self, testing_engine):
  200. levels = requirements.get_isolation_levels(config)
  201. default = levels["default"]
  202. supported = (
  203. sorted(
  204. set(levels["supported"]).difference([default, "AUTOCOMMIT"])
  205. )
  206. )[0]
  207. e = testing_engine(options={"isolation_level": supported})
  208. with e.connect() as conn:
  209. eq_(conn.get_isolation_level(), supported)
  210. with e.connect() as conn:
  211. conn.execution_options(isolation_level=default)
  212. eq_(conn.get_isolation_level(), default)
  213. with e.connect() as conn:
  214. eq_(conn.get_isolation_level(), supported)
  215. class AutocommitIsolationTest(fixtures.TablesTest):
  216. run_deletes = "each"
  217. __requires__ = ("autocommit",)
  218. __backend__ = True
  219. @classmethod
  220. def define_tables(cls, metadata):
  221. Table(
  222. "some_table",
  223. metadata,
  224. Column("id", Integer, primary_key=True, autoincrement=False),
  225. Column("data", String(50)),
  226. test_needs_acid=True,
  227. )
  228. def _test_conn_autocommits(self, conn, autocommit):
  229. trans = conn.begin()
  230. conn.execute(
  231. self.tables.some_table.insert(), {"id": 1, "data": "some data"}
  232. )
  233. trans.rollback()
  234. eq_(
  235. conn.scalar(select(self.tables.some_table.c.id)),
  236. 1 if autocommit else None,
  237. )
  238. conn.rollback()
  239. with conn.begin():
  240. conn.execute(self.tables.some_table.delete())
  241. def test_autocommit_on(self, connection_no_trans):
  242. conn = connection_no_trans
  243. c2 = conn.execution_options(isolation_level="AUTOCOMMIT")
  244. self._test_conn_autocommits(c2, True)
  245. c2.dialect.reset_isolation_level(c2.connection.dbapi_connection)
  246. self._test_conn_autocommits(conn, False)
  247. def test_autocommit_off(self, connection_no_trans):
  248. conn = connection_no_trans
  249. self._test_conn_autocommits(conn, False)
  250. def test_turn_autocommit_off_via_default_iso_level(
  251. self, connection_no_trans
  252. ):
  253. conn = connection_no_trans
  254. conn = conn.execution_options(isolation_level="AUTOCOMMIT")
  255. self._test_conn_autocommits(conn, True)
  256. conn.execution_options(
  257. isolation_level=requirements.get_isolation_levels(config)[
  258. "default"
  259. ]
  260. )
  261. self._test_conn_autocommits(conn, False)
  262. @testing.requires.independent_readonly_connections
  263. @testing.variation("use_dialect_setting", [True, False])
  264. def test_dialect_autocommit_is_restored(
  265. self, testing_engine, use_dialect_setting
  266. ):
  267. """test #10147"""
  268. if use_dialect_setting:
  269. e = testing_engine(options={"isolation_level": "AUTOCOMMIT"})
  270. else:
  271. e = testing_engine().execution_options(
  272. isolation_level="AUTOCOMMIT"
  273. )
  274. levels = requirements.get_isolation_levels(config)
  275. default = levels["default"]
  276. with e.connect() as conn:
  277. self._test_conn_autocommits(conn, True)
  278. with e.connect() as conn:
  279. conn.execution_options(isolation_level=default)
  280. self._test_conn_autocommits(conn, False)
  281. with e.connect() as conn:
  282. self._test_conn_autocommits(conn, True)
  283. class EscapingTest(fixtures.TestBase):
  284. @provide_metadata
  285. def test_percent_sign_round_trip(self):
  286. """test that the DBAPI accommodates for escaped / nonescaped
  287. percent signs in a way that matches the compiler
  288. """
  289. m = self.metadata
  290. t = Table("t", m, Column("data", String(50)))
  291. t.create(config.db)
  292. with config.db.begin() as conn:
  293. conn.execute(t.insert(), dict(data="some % value"))
  294. conn.execute(t.insert(), dict(data="some %% other value"))
  295. eq_(
  296. conn.scalar(
  297. select(t.c.data).where(
  298. t.c.data == literal_column("'some % value'")
  299. )
  300. ),
  301. "some % value",
  302. )
  303. eq_(
  304. conn.scalar(
  305. select(t.c.data).where(
  306. t.c.data == literal_column("'some %% other value'")
  307. )
  308. ),
  309. "some %% other value",
  310. )
  311. class WeCanSetDefaultSchemaWEventsTest(fixtures.TestBase):
  312. __backend__ = True
  313. __requires__ = ("default_schema_name_switch",)
  314. def test_control_case(self):
  315. default_schema_name = config.db.dialect.default_schema_name
  316. eng = engines.testing_engine()
  317. with eng.connect():
  318. pass
  319. eq_(eng.dialect.default_schema_name, default_schema_name)
  320. def test_wont_work_wo_insert(self):
  321. default_schema_name = config.db.dialect.default_schema_name
  322. eng = engines.testing_engine()
  323. @event.listens_for(eng, "connect")
  324. def on_connect(dbapi_connection, connection_record):
  325. set_default_schema_on_connection(
  326. config, dbapi_connection, config.test_schema
  327. )
  328. with eng.connect() as conn:
  329. what_it_should_be = eng.dialect._get_default_schema_name(conn)
  330. eq_(what_it_should_be, config.test_schema)
  331. eq_(eng.dialect.default_schema_name, default_schema_name)
  332. def test_schema_change_on_connect(self):
  333. eng = engines.testing_engine()
  334. @event.listens_for(eng, "connect", insert=True)
  335. def on_connect(dbapi_connection, connection_record):
  336. set_default_schema_on_connection(
  337. config, dbapi_connection, config.test_schema
  338. )
  339. with eng.connect() as conn:
  340. what_it_should_be = eng.dialect._get_default_schema_name(conn)
  341. eq_(what_it_should_be, config.test_schema)
  342. eq_(eng.dialect.default_schema_name, config.test_schema)
  343. def test_schema_change_works_w_transactions(self):
  344. eng = engines.testing_engine()
  345. @event.listens_for(eng, "connect", insert=True)
  346. def on_connect(dbapi_connection, *arg):
  347. set_default_schema_on_connection(
  348. config, dbapi_connection, config.test_schema
  349. )
  350. with eng.connect() as conn:
  351. trans = conn.begin()
  352. what_it_should_be = eng.dialect._get_default_schema_name(conn)
  353. eq_(what_it_should_be, config.test_schema)
  354. trans.rollback()
  355. what_it_should_be = eng.dialect._get_default_schema_name(conn)
  356. eq_(what_it_should_be, config.test_schema)
  357. eq_(eng.dialect.default_schema_name, config.test_schema)
  358. class FutureWeCanSetDefaultSchemaWEventsTest(
  359. fixtures.FutureEngineMixin, WeCanSetDefaultSchemaWEventsTest
  360. ):
  361. pass
  362. class DifficultParametersTest(fixtures.TestBase):
  363. __backend__ = True
  364. tough_parameters = testing.combinations(
  365. ("boring",),
  366. ("per cent",),
  367. ("per % cent",),
  368. ("%percent",),
  369. ("par(ens)",),
  370. ("percent%(ens)yah",),
  371. ("col:ons",),
  372. ("_starts_with_underscore",),
  373. ("dot.s",),
  374. ("more :: %colons%",),
  375. ("_name",),
  376. ("___name",),
  377. ("[BracketsAndCase]",),
  378. ("42numbers",),
  379. ("percent%signs",),
  380. ("has spaces",),
  381. ("/slashes/",),
  382. ("more/slashes",),
  383. ("q?marks",),
  384. ("1param",),
  385. ("1col:on",),
  386. argnames="paramname",
  387. )
  388. @tough_parameters
  389. @config.requirements.unusual_column_name_characters
  390. def test_round_trip_same_named_column(
  391. self, paramname, connection, metadata
  392. ):
  393. name = paramname
  394. t = Table(
  395. "t",
  396. metadata,
  397. Column("id", Integer, primary_key=True),
  398. Column(name, String(50), nullable=False),
  399. )
  400. # table is created
  401. t.create(connection)
  402. # automatic param generated by insert
  403. connection.execute(t.insert().values({"id": 1, name: "some name"}))
  404. # automatic param generated by criteria, plus selecting the column
  405. stmt = select(t.c[name]).where(t.c[name] == "some name")
  406. eq_(connection.scalar(stmt), "some name")
  407. # use the name in a param explicitly
  408. stmt = select(t.c[name]).where(t.c[name] == bindparam(name))
  409. row = connection.execute(stmt, {name: "some name"}).first()
  410. # name works as the key from cursor.description
  411. eq_(row._mapping[name], "some name")
  412. # use expanding IN
  413. stmt = select(t.c[name]).where(
  414. t.c[name].in_(["some name", "some other_name"])
  415. )
  416. row = connection.execute(stmt).first()
  417. @testing.fixture
  418. def multirow_fixture(self, metadata, connection):
  419. mytable = Table(
  420. "mytable",
  421. metadata,
  422. Column("myid", Integer),
  423. Column("name", String(50)),
  424. Column("desc", String(50)),
  425. )
  426. mytable.create(connection)
  427. connection.execute(
  428. mytable.insert(),
  429. [
  430. {"myid": 1, "name": "a", "desc": "a_desc"},
  431. {"myid": 2, "name": "b", "desc": "b_desc"},
  432. {"myid": 3, "name": "c", "desc": "c_desc"},
  433. {"myid": 4, "name": "d", "desc": "d_desc"},
  434. ],
  435. )
  436. yield mytable
  437. @tough_parameters
  438. def test_standalone_bindparam_escape(
  439. self, paramname, connection, multirow_fixture
  440. ):
  441. tbl1 = multirow_fixture
  442. stmt = select(tbl1.c.myid).where(
  443. tbl1.c.name == bindparam(paramname, value="x")
  444. )
  445. res = connection.scalar(stmt, {paramname: "c"})
  446. eq_(res, 3)
  447. @tough_parameters
  448. def test_standalone_bindparam_escape_expanding(
  449. self, paramname, connection, multirow_fixture
  450. ):
  451. tbl1 = multirow_fixture
  452. stmt = (
  453. select(tbl1.c.myid)
  454. .where(tbl1.c.name.in_(bindparam(paramname, value=["a", "b"])))
  455. .order_by(tbl1.c.myid)
  456. )
  457. res = connection.scalars(stmt, {paramname: ["d", "a"]}).all()
  458. eq_(res, [1, 4])
  459. class ReturningGuardsTest(fixtures.TablesTest):
  460. """test that the various 'returning' flags are set appropriately"""
  461. __backend__ = True
  462. @classmethod
  463. def define_tables(cls, metadata):
  464. Table(
  465. "t",
  466. metadata,
  467. Column("id", Integer, primary_key=True, autoincrement=False),
  468. Column("data", String(50)),
  469. )
  470. @testing.fixture
  471. def run_stmt(self, connection):
  472. t = self.tables.t
  473. def go(stmt, executemany, id_param_name, expect_success):
  474. stmt = stmt.returning(t.c.id)
  475. if executemany:
  476. if not expect_success:
  477. # for RETURNING executemany(), we raise our own
  478. # error as this is independent of general RETURNING
  479. # support
  480. with expect_raises_message(
  481. exc.StatementError,
  482. rf"Dialect {connection.dialect.name}\+"
  483. f"{connection.dialect.driver} with "
  484. f"current server capabilities does not support "
  485. f".*RETURNING when executemany is used",
  486. ):
  487. result = connection.execute(
  488. stmt,
  489. [
  490. {id_param_name: 1, "data": "d1"},
  491. {id_param_name: 2, "data": "d2"},
  492. {id_param_name: 3, "data": "d3"},
  493. ],
  494. )
  495. else:
  496. result = connection.execute(
  497. stmt,
  498. [
  499. {id_param_name: 1, "data": "d1"},
  500. {id_param_name: 2, "data": "d2"},
  501. {id_param_name: 3, "data": "d3"},
  502. ],
  503. )
  504. eq_(result.all(), [(1,), (2,), (3,)])
  505. else:
  506. if not expect_success:
  507. # for RETURNING execute(), we pass all the way to the DB
  508. # and let it fail
  509. with expect_raises(exc.DBAPIError):
  510. connection.execute(
  511. stmt, {id_param_name: 1, "data": "d1"}
  512. )
  513. else:
  514. result = connection.execute(
  515. stmt, {id_param_name: 1, "data": "d1"}
  516. )
  517. eq_(result.all(), [(1,)])
  518. return go
  519. def test_insert_single(self, connection, run_stmt):
  520. t = self.tables.t
  521. stmt = t.insert()
  522. run_stmt(stmt, False, "id", connection.dialect.insert_returning)
  523. def test_insert_many(self, connection, run_stmt):
  524. t = self.tables.t
  525. stmt = t.insert()
  526. run_stmt(
  527. stmt, True, "id", connection.dialect.insert_executemany_returning
  528. )
  529. def test_update_single(self, connection, run_stmt):
  530. t = self.tables.t
  531. connection.execute(
  532. t.insert(),
  533. [
  534. {"id": 1, "data": "d1"},
  535. {"id": 2, "data": "d2"},
  536. {"id": 3, "data": "d3"},
  537. ],
  538. )
  539. stmt = t.update().where(t.c.id == bindparam("b_id"))
  540. run_stmt(stmt, False, "b_id", connection.dialect.update_returning)
  541. def test_update_many(self, connection, run_stmt):
  542. t = self.tables.t
  543. connection.execute(
  544. t.insert(),
  545. [
  546. {"id": 1, "data": "d1"},
  547. {"id": 2, "data": "d2"},
  548. {"id": 3, "data": "d3"},
  549. ],
  550. )
  551. stmt = t.update().where(t.c.id == bindparam("b_id"))
  552. run_stmt(
  553. stmt, True, "b_id", connection.dialect.update_executemany_returning
  554. )
  555. def test_delete_single(self, connection, run_stmt):
  556. t = self.tables.t
  557. connection.execute(
  558. t.insert(),
  559. [
  560. {"id": 1, "data": "d1"},
  561. {"id": 2, "data": "d2"},
  562. {"id": 3, "data": "d3"},
  563. ],
  564. )
  565. stmt = t.delete().where(t.c.id == bindparam("b_id"))
  566. run_stmt(stmt, False, "b_id", connection.dialect.delete_returning)
  567. def test_delete_many(self, connection, run_stmt):
  568. t = self.tables.t
  569. connection.execute(
  570. t.insert(),
  571. [
  572. {"id": 1, "data": "d1"},
  573. {"id": 2, "data": "d2"},
  574. {"id": 3, "data": "d3"},
  575. ],
  576. )
  577. stmt = t.delete().where(t.c.id == bindparam("b_id"))
  578. run_stmt(
  579. stmt, True, "b_id", connection.dialect.delete_executemany_returning
  580. )