provision.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. # dialects/sqlite/provision.py
  2. # Copyright (C) 2005-2026 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 os
  9. import re
  10. from ... import event
  11. from ... import exc
  12. from ...engine import url as sa_url
  13. from ...testing import config
  14. from ...testing.provision import create_db
  15. from ...testing.provision import drop_db
  16. from ...testing.provision import follower_url_from_main
  17. from ...testing.provision import generate_driver_url
  18. from ...testing.provision import log
  19. from ...testing.provision import post_configure_engine
  20. from ...testing.provision import post_configure_testing_engine
  21. from ...testing.provision import run_reap_dbs
  22. from ...testing.provision import stop_test_class_outside_fixtures
  23. from ...testing.provision import temp_table_keyword_args
  24. from ...testing.provision import upsert
  25. # TODO: I can't get this to build dynamically with pytest-xdist procs
  26. _drivernames = {
  27. "pysqlite",
  28. "aiosqlite",
  29. "pysqlcipher",
  30. "pysqlite_numeric",
  31. "pysqlite_dollar",
  32. }
  33. def _format_url(url, driver, ident):
  34. """given a sqlite url + desired driver + ident, make a canonical
  35. URL out of it
  36. """
  37. url = sa_url.make_url(url)
  38. if driver is None:
  39. driver = url.get_driver_name()
  40. filename = url.database
  41. needs_enc = driver == "pysqlcipher"
  42. name_token = None
  43. if filename and filename != ":memory:":
  44. assert "test_schema" not in filename
  45. tokens = re.split(r"[_\.]", filename)
  46. for token in tokens:
  47. if token in _drivernames:
  48. if driver is None:
  49. driver = token
  50. continue
  51. elif token in ("db", "enc"):
  52. continue
  53. elif name_token is None:
  54. name_token = token.strip("_")
  55. assert name_token, f"sqlite filename has no name token: {url.database}"
  56. new_filename = f"{name_token}_{driver}"
  57. if ident:
  58. new_filename += f"_{ident}"
  59. new_filename += ".db"
  60. if needs_enc:
  61. new_filename += ".enc"
  62. url = url.set(database=new_filename)
  63. if needs_enc:
  64. url = url.set(password="test")
  65. url = url.set(drivername="sqlite+%s" % (driver,))
  66. return url
  67. @generate_driver_url.for_db("sqlite")
  68. def generate_driver_url(url, driver, query_str):
  69. url = _format_url(url, driver, None)
  70. try:
  71. url.get_dialect()
  72. except exc.NoSuchModuleError:
  73. return None
  74. else:
  75. return url
  76. @follower_url_from_main.for_db("sqlite")
  77. def _sqlite_follower_url_from_main(url, ident):
  78. return _format_url(url, None, ident)
  79. @post_configure_engine.for_db("sqlite")
  80. def _sqlite_post_configure_engine(url, engine, follower_ident):
  81. from sqlalchemy import event
  82. if follower_ident:
  83. attach_path = f"{follower_ident}_{engine.driver}_test_schema.db"
  84. else:
  85. attach_path = f"{engine.driver}_test_schema.db"
  86. @event.listens_for(engine, "connect")
  87. def connect(dbapi_connection, connection_record):
  88. # use file DBs in all cases, memory acts kind of strangely
  89. # as an attached
  90. # NOTE! this has to be done *per connection*. New sqlite connection,
  91. # as we get with say, QueuePool, the attaches are gone.
  92. # so schemes to delete those attached files have to be done at the
  93. # filesystem level and not rely upon what attachments are in a
  94. # particular SQLite connection
  95. dbapi_connection.execute(
  96. f'ATTACH DATABASE "{attach_path}" AS test_schema'
  97. )
  98. @event.listens_for(engine, "engine_disposed")
  99. def dispose(engine):
  100. """most databases should be dropped using
  101. stop_test_class_outside_fixtures
  102. however a few tests like AttachedDBTest might not get triggered on
  103. that main hook
  104. """
  105. if os.path.exists(attach_path):
  106. os.remove(attach_path)
  107. filename = engine.url.database
  108. if filename and filename != ":memory:" and os.path.exists(filename):
  109. os.remove(filename)
  110. @post_configure_testing_engine.for_db("sqlite")
  111. def _sqlite_post_configure_testing_engine(url, engine, options, scope):
  112. sqlite_savepoint = options.get("sqlite_savepoint", False)
  113. sqlite_share_pool = options.get("sqlite_share_pool", False)
  114. if sqlite_savepoint and engine.name == "sqlite":
  115. # apply SQLite savepoint workaround
  116. @event.listens_for(engine, "connect")
  117. def do_connect(dbapi_connection, connection_record):
  118. dbapi_connection.isolation_level = None
  119. @event.listens_for(engine, "begin")
  120. def do_begin(conn):
  121. conn.exec_driver_sql("BEGIN")
  122. if sqlite_share_pool:
  123. # SingletonThreadPool, StaticPool both support "transfer"
  124. # so a new pool can share the same SQLite connection
  125. # (single thread only)
  126. if hasattr(engine.pool, "_transfer_from"):
  127. options["use_reaper"] = False
  128. engine.pool._transfer_from(config.db.pool)
  129. @create_db.for_db("sqlite")
  130. def _sqlite_create_db(cfg, eng, ident):
  131. pass
  132. @drop_db.for_db("sqlite")
  133. def _sqlite_drop_db(cfg, eng, ident):
  134. _drop_dbs_w_ident(eng.url.database, eng.driver, ident)
  135. def _drop_dbs_w_ident(databasename, driver, ident):
  136. for path in os.listdir("."):
  137. fname, ext = os.path.split(path)
  138. if ident in fname and ext in [".db", ".db.enc"]:
  139. log.info("deleting SQLite database file: %s", path)
  140. os.remove(path)
  141. @stop_test_class_outside_fixtures.for_db("sqlite")
  142. def stop_test_class_outside_fixtures(config, db, cls):
  143. db.dispose()
  144. @temp_table_keyword_args.for_db("sqlite")
  145. def _sqlite_temp_table_keyword_args(cfg, eng):
  146. return {"prefixes": ["TEMPORARY"]}
  147. @run_reap_dbs.for_db("sqlite")
  148. def _reap_sqlite_dbs(url, idents):
  149. log.info("db reaper connecting to %r", url)
  150. log.info("identifiers in file: %s", ", ".join(idents))
  151. url = sa_url.make_url(url)
  152. for ident in idents:
  153. for drivername in _drivernames:
  154. _drop_dbs_w_ident(url.database, drivername, ident)
  155. @upsert.for_db("sqlite")
  156. def _upsert(
  157. cfg,
  158. table,
  159. returning,
  160. *,
  161. set_lambda=None,
  162. sort_by_parameter_order=False,
  163. index_elements=None,
  164. ):
  165. from sqlalchemy.dialects.sqlite import insert
  166. stmt = insert(table)
  167. if set_lambda:
  168. stmt = stmt.on_conflict_do_update(set_=set_lambda(stmt.excluded))
  169. else:
  170. stmt = stmt.on_conflict_do_nothing()
  171. stmt = stmt.returning(
  172. *returning, sort_by_parameter_order=sort_by_parameter_order
  173. )
  174. return stmt