Team Ai
Datasetpublic

codekingpro/portable-devtools

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