SQLAlchemy 2.0.51__py3-none-any.whl → 2.0.53__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlalchemy/__init__.py +1 -1
- sqlalchemy/connectors/pyodbc.py +13 -4
- sqlalchemy/dialects/mssql/base.py +11 -3
- sqlalchemy/dialects/mysql/base.py +2 -2
- sqlalchemy/dialects/oracle/oracledb.py +21 -1
- sqlalchemy/dialects/postgresql/base.py +77 -13
- sqlalchemy/dialects/postgresql/json.py +3 -0
- sqlalchemy/dialects/sqlite/aiosqlite.py +30 -0
- sqlalchemy/dialects/sqlite/base.py +83 -41
- sqlalchemy/dialects/sqlite/pysqlite.py +89 -12
- sqlalchemy/engine/reflection.py +9 -0
- sqlalchemy/ext/asyncio/engine.py +6 -6
- sqlalchemy/ext/asyncio/scoping.py +12 -9
- sqlalchemy/ext/asyncio/session.py +6 -5
- sqlalchemy/orm/bulk_persistence.py +16 -0
- sqlalchemy/orm/decl_base.py +14 -0
- sqlalchemy/orm/path_registry.py +55 -13
- sqlalchemy/orm/properties.py +2 -2
- sqlalchemy/orm/relationships.py +13 -6
- sqlalchemy/orm/scoping.py +7 -5
- sqlalchemy/orm/session.py +25 -23
- sqlalchemy/orm/strategies.py +10 -3
- sqlalchemy/orm/strategy_options.py +11 -6
- sqlalchemy/orm/util.py +73 -4
- sqlalchemy/pool/base.py +29 -11
- sqlalchemy/sql/_elements_constructors.py +12 -0
- sqlalchemy/sql/compiler.py +27 -1
- sqlalchemy/sql/elements.py +7 -2
- sqlalchemy/sql/schema.py +16 -17
- sqlalchemy/sql/selectable.py +19 -9
- sqlalchemy/sql/sqltypes.py +9 -5
- sqlalchemy/testing/__init__.py +2 -0
- sqlalchemy/testing/cancellation.py +237 -0
- sqlalchemy/testing/config.py +7 -0
- sqlalchemy/testing/fixtures/mypy.py +5 -3
- sqlalchemy/testing/fixtures/orm.py +2 -2
- sqlalchemy/testing/fixtures/sql.py +2 -2
- sqlalchemy/testing/plugin/plugin_base.py +4 -0
- sqlalchemy/testing/plugin/pytestplugin.py +30 -0
- sqlalchemy/testing/requirements.py +23 -0
- sqlalchemy/testing/suite/test_dialect.py +1 -0
- sqlalchemy/testing/suite/test_reflection.py +138 -0
- sqlalchemy/testing/suite/test_update_delete.py +37 -0
- sqlalchemy/testing/util.py +41 -0
- sqlalchemy/util/__init__.py +2 -0
- sqlalchemy/util/compat.py +1 -1
- sqlalchemy/util/langhelpers.py +105 -2
- sqlalchemy/util/typing.py +2 -0
- {sqlalchemy-2.0.51.dist-info → sqlalchemy-2.0.53.dist-info}/METADATA +5 -2
- {sqlalchemy-2.0.51.dist-info → sqlalchemy-2.0.53.dist-info}/RECORD +54 -52
- {sqlalchemy-2.0.51.dist-info → sqlalchemy-2.0.53.dist-info}/WHEEL +1 -1
- sqlalchemy-2.0.53.dist-info/licenses/AUTHORS +30 -0
- {sqlalchemy-2.0.51.dist-info → sqlalchemy-2.0.53.dist-info}/licenses/LICENSE +0 -0
- {sqlalchemy-2.0.51.dist-info → sqlalchemy-2.0.53.dist-info}/top_level.txt +0 -0
sqlalchemy/__init__.py
CHANGED
sqlalchemy/connectors/pyodbc.py
CHANGED
|
@@ -77,10 +77,16 @@ class PyODBCConnector(Connector):
|
|
|
77
77
|
else:
|
|
78
78
|
|
|
79
79
|
def check_quote(token: str) -> str:
|
|
80
|
-
if
|
|
80
|
+
if (
|
|
81
|
+
";" in str(token)
|
|
82
|
+
or "}" in str(token)
|
|
83
|
+
or str(token).startswith("{")
|
|
84
|
+
):
|
|
81
85
|
token = "{%s}" % token.replace("}", "}}")
|
|
82
86
|
return token
|
|
83
87
|
|
|
88
|
+
driver = keys.pop("driver", self.pyodbc_driver_name)
|
|
89
|
+
|
|
84
90
|
keys = {k: check_quote(v) for k, v in keys.items()}
|
|
85
91
|
|
|
86
92
|
dsn_connection = "dsn" in keys or (
|
|
@@ -96,7 +102,6 @@ class PyODBCConnector(Connector):
|
|
|
96
102
|
port = ",%d" % int(keys.pop("port"))
|
|
97
103
|
|
|
98
104
|
connectors = []
|
|
99
|
-
driver = keys.pop("driver", self.pyodbc_driver_name)
|
|
100
105
|
if driver is None and keys:
|
|
101
106
|
# note if keys is empty, this is a totally blank URL
|
|
102
107
|
util.warn(
|
|
@@ -105,7 +110,9 @@ class PyODBCConnector(Connector):
|
|
|
105
110
|
"DSN-less connections"
|
|
106
111
|
)
|
|
107
112
|
else:
|
|
108
|
-
connectors.append(
|
|
113
|
+
connectors.append(
|
|
114
|
+
"DRIVER={%s}" % str(driver).replace("}", "}}")
|
|
115
|
+
)
|
|
109
116
|
|
|
110
117
|
connectors.extend(
|
|
111
118
|
[
|
|
@@ -136,7 +143,9 @@ class PyODBCConnector(Connector):
|
|
|
136
143
|
"AutoTranslate=%s" % keys.pop("odbc_autotranslate")
|
|
137
144
|
)
|
|
138
145
|
|
|
139
|
-
connectors.extend(
|
|
146
|
+
connectors.extend(
|
|
147
|
+
["%s=%s" % (check_quote(k), v) for k, v in keys.items()]
|
|
148
|
+
)
|
|
140
149
|
|
|
141
150
|
return ((";".join(connectors),), connect_args)
|
|
142
151
|
|
|
@@ -1585,7 +1585,7 @@ class MSUUid(sqltypes.Uuid):
|
|
|
1585
1585
|
if self.native_uuid:
|
|
1586
1586
|
|
|
1587
1587
|
def process(value):
|
|
1588
|
-
return f"""'{str(value).replace("'
|
|
1588
|
+
return f"""'{str(value).replace("'", "''")}'"""
|
|
1589
1589
|
|
|
1590
1590
|
return process
|
|
1591
1591
|
else:
|
|
@@ -3780,7 +3780,6 @@ order by
|
|
|
3780
3780
|
elif coltype in (
|
|
3781
3781
|
MSString,
|
|
3782
3782
|
MSChar,
|
|
3783
|
-
MSText,
|
|
3784
3783
|
):
|
|
3785
3784
|
kwargs["length"] = maxlen if maxlen != -1 else None
|
|
3786
3785
|
if collation:
|
|
@@ -3788,11 +3787,20 @@ order by
|
|
|
3788
3787
|
elif coltype in (
|
|
3789
3788
|
MSNVarchar,
|
|
3790
3789
|
MSNChar,
|
|
3791
|
-
MSNText,
|
|
3792
3790
|
):
|
|
3793
3791
|
kwargs["length"] = maxlen // 2 if maxlen != -1 else None
|
|
3794
3792
|
if collation:
|
|
3795
3793
|
kwargs["collation"] = collation
|
|
3794
|
+
elif coltype in (
|
|
3795
|
+
MSText,
|
|
3796
|
+
MSNText,
|
|
3797
|
+
):
|
|
3798
|
+
# TEXT / NTEXT are unlengthed LOB types.
|
|
3799
|
+
# sys.columns.max_length reports 16 for these, which is the
|
|
3800
|
+
# size of the in-row LOB pointer and not a character length,
|
|
3801
|
+
# so no length is applied.
|
|
3802
|
+
if collation:
|
|
3803
|
+
kwargs["collation"] = collation
|
|
3796
3804
|
|
|
3797
3805
|
if coltype is None:
|
|
3798
3806
|
if base_type is not None and base_type != type_:
|
|
@@ -2105,8 +2105,8 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
|
|
|
2105
2105
|
"SUBPARTITION_BY",
|
|
2106
2106
|
]
|
|
2107
2107
|
|
|
2108
|
-
nonpart_options =
|
|
2109
|
-
part_options =
|
|
2108
|
+
nonpart_options = util.OrderedSet(opts).difference(partition_options)
|
|
2109
|
+
part_options = util.OrderedSet(opts).intersection(partition_options)
|
|
2110
2110
|
|
|
2111
2111
|
for opt in topological.sort(
|
|
2112
2112
|
[
|
|
@@ -602,6 +602,7 @@ from ... import pool
|
|
|
602
602
|
from ...connectors.asyncio import AsyncAdapt_dbapi_connection
|
|
603
603
|
from ...connectors.asyncio import AsyncAdapt_dbapi_cursor
|
|
604
604
|
from ...connectors.asyncio import AsyncAdapt_dbapi_ss_cursor
|
|
605
|
+
from ...connectors.asyncio import AsyncAdapt_terminate
|
|
605
606
|
from ...connectors.asyncio import AsyncAdaptFallback_dbapi_connection
|
|
606
607
|
from ...engine import default
|
|
607
608
|
from ...util import asbool
|
|
@@ -795,7 +796,9 @@ class AsyncAdapt_oracledb_ss_cursor(
|
|
|
795
796
|
self._cursor = None # type: ignore
|
|
796
797
|
|
|
797
798
|
|
|
798
|
-
class AsyncAdapt_oracledb_connection(
|
|
799
|
+
class AsyncAdapt_oracledb_connection(
|
|
800
|
+
AsyncAdapt_terminate, AsyncAdapt_dbapi_connection
|
|
801
|
+
):
|
|
799
802
|
_connection: AsyncConnection
|
|
800
803
|
__slots__ = ()
|
|
801
804
|
|
|
@@ -860,6 +863,12 @@ class AsyncAdapt_oracledb_connection(AsyncAdapt_dbapi_connection):
|
|
|
860
863
|
def tpc_rollback(self, *args: Any, **kwargs: Any) -> Any:
|
|
861
864
|
return self.await_(self._connection.tpc_rollback(*args, **kwargs))
|
|
862
865
|
|
|
866
|
+
async def _terminate_graceful_close(self) -> None:
|
|
867
|
+
await self._connection.close()
|
|
868
|
+
|
|
869
|
+
def _terminate_force_close(self) -> None:
|
|
870
|
+
self._connection.terminate()
|
|
871
|
+
|
|
863
872
|
|
|
864
873
|
class AsyncAdaptFallback_oracledb_connection(
|
|
865
874
|
AsyncAdaptFallback_dbapi_connection, AsyncAdapt_oracledb_connection
|
|
@@ -918,6 +927,11 @@ class OracleDialectAsync_oracledb(OracleDialect_oracledb):
|
|
|
918
927
|
|
|
919
928
|
_min_version = (2,)
|
|
920
929
|
|
|
930
|
+
def __init__(self, **kwargs):
|
|
931
|
+
super().__init__(**kwargs)
|
|
932
|
+
|
|
933
|
+
self.has_terminate = self.oracledb_ver >= (26, 0, 0)
|
|
934
|
+
|
|
921
935
|
# thick_mode mode is not supported by asyncio, oracledb will raise
|
|
922
936
|
@classmethod
|
|
923
937
|
def import_dbapi(cls):
|
|
@@ -937,6 +951,12 @@ class OracleDialectAsync_oracledb(OracleDialect_oracledb):
|
|
|
937
951
|
def get_driver_connection(self, connection):
|
|
938
952
|
return connection._connection
|
|
939
953
|
|
|
954
|
+
def do_terminate(self, dbapi_connection) -> None:
|
|
955
|
+
if self.has_terminate:
|
|
956
|
+
dbapi_connection.terminate()
|
|
957
|
+
else:
|
|
958
|
+
super().do_terminate(dbapi_connection)
|
|
959
|
+
|
|
940
960
|
|
|
941
961
|
dialect = OracleDialect_oracledb
|
|
942
962
|
dialect_async = OracleDialectAsync_oracledb
|
|
@@ -1213,6 +1213,33 @@ identifier::
|
|
|
1213
1213
|
),
|
|
1214
1214
|
)
|
|
1215
1215
|
|
|
1216
|
+
SQLAlchemy does not automatically copy the columns from the inherited tables
|
|
1217
|
+
mentioned in the ``postgresql_inherits`` argument into the new
|
|
1218
|
+
:class:`_schema.Table`. To populate the new table columns reflection may be
|
|
1219
|
+
used, or a function similar to the following one::
|
|
1220
|
+
|
|
1221
|
+
def get_parent_columns(tbl: sa.Table) -> list[sa.Column]:
|
|
1222
|
+
return [
|
|
1223
|
+
sa.Column(
|
|
1224
|
+
c.name,
|
|
1225
|
+
c.type,
|
|
1226
|
+
key=c.key,
|
|
1227
|
+
nullable=c.nullable,
|
|
1228
|
+
# Set system=true to omit from the CREATE TABLE statement
|
|
1229
|
+
system=True,
|
|
1230
|
+
)
|
|
1231
|
+
for c in tbl.columns
|
|
1232
|
+
]
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
documents = Table(
|
|
1236
|
+
"some_table",
|
|
1237
|
+
metadata,
|
|
1238
|
+
*get_parent_columns(some_supertable),
|
|
1239
|
+
# ... # add other columns here normally if needed
|
|
1240
|
+
postgresql_inherits="some_supertable",
|
|
1241
|
+
)
|
|
1242
|
+
|
|
1216
1243
|
``ON COMMIT``
|
|
1217
1244
|
^^^^^^^^^^^^^
|
|
1218
1245
|
|
|
@@ -2135,7 +2162,9 @@ class PGCompiler(compiler.SQLCompiler):
|
|
|
2135
2162
|
return "string_agg%s" % self.function_argspec(fn)
|
|
2136
2163
|
|
|
2137
2164
|
def visit_sequence(self, seq, **kw):
|
|
2138
|
-
return "nextval(
|
|
2165
|
+
return "nextval(%s)" % self.preparer._format_sequence_string_literal(
|
|
2166
|
+
seq
|
|
2167
|
+
)
|
|
2139
2168
|
|
|
2140
2169
|
def limit_clause(self, select, **kw):
|
|
2141
2170
|
text = ""
|
|
@@ -2968,9 +2997,43 @@ class PGTypeCompiler(compiler.GenericTypeCompiler):
|
|
|
2968
2997
|
return "JSONPATH"
|
|
2969
2998
|
|
|
2970
2999
|
|
|
3000
|
+
class _CompilerSequence:
|
|
3001
|
+
"""Minimal stand-in for :class:`.Sequence`.
|
|
3002
|
+
|
|
3003
|
+
Used for the implicit sequence behind a SERIAL column, where no
|
|
3004
|
+
:class:`.Sequence` object exists but a name still has to be rendered
|
|
3005
|
+
through :meth:`.IdentifierPreparer.format_sequence`.
|
|
3006
|
+
|
|
3007
|
+
"""
|
|
3008
|
+
|
|
3009
|
+
__slots__ = ("name", "schema")
|
|
3010
|
+
|
|
3011
|
+
# the schema handed to us has already been resolved against the
|
|
3012
|
+
# schema translate map, so don't let format_sequence() translate it
|
|
3013
|
+
# a second time
|
|
3014
|
+
_use_schema_map = False
|
|
3015
|
+
|
|
3016
|
+
def __init__(self, name, schema=None):
|
|
3017
|
+
self.name = name
|
|
3018
|
+
self.schema = schema
|
|
3019
|
+
|
|
3020
|
+
|
|
2971
3021
|
class PGIdentifierPreparer(compiler.IdentifierPreparer):
|
|
2972
3022
|
reserved_words = RESERVED_WORDS
|
|
2973
3023
|
|
|
3024
|
+
def _format_sequence_string_literal(self, sequence):
|
|
3025
|
+
"""Render a sequence name as a SQL string literal.
|
|
3026
|
+
|
|
3027
|
+
``nextval()`` and friends take the sequence as a string rather than
|
|
3028
|
+
as an identifier, so the quoted identifier produced by
|
|
3029
|
+
:meth:`.format_sequence` has to be escaped a second time for the
|
|
3030
|
+
enclosing literal.
|
|
3031
|
+
|
|
3032
|
+
"""
|
|
3033
|
+
return "'%s'" % self.format_sequence(
|
|
3034
|
+
sequence, use_schema=True
|
|
3035
|
+
).replace("'", "''")
|
|
3036
|
+
|
|
2974
3037
|
def _unquote_identifier(self, value):
|
|
2975
3038
|
if value[0] == self.initial_quote:
|
|
2976
3039
|
value = value[1:-1].replace(
|
|
@@ -3156,8 +3219,8 @@ class PGExecutionContext(default.DefaultExecutionContext):
|
|
|
3156
3219
|
def fire_sequence(self, seq, type_):
|
|
3157
3220
|
return self._execute_scalar(
|
|
3158
3221
|
(
|
|
3159
|
-
"select nextval(
|
|
3160
|
-
% self.identifier_preparer.
|
|
3222
|
+
"select nextval(%s)"
|
|
3223
|
+
% self.identifier_preparer._format_sequence_string_literal(seq)
|
|
3161
3224
|
),
|
|
3162
3225
|
type_,
|
|
3163
3226
|
)
|
|
@@ -3194,13 +3257,12 @@ class PGExecutionContext(default.DefaultExecutionContext):
|
|
|
3194
3257
|
else:
|
|
3195
3258
|
effective_schema = None
|
|
3196
3259
|
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
seq_name,
|
|
3260
|
+
exc = (
|
|
3261
|
+
"select nextval(%s)"
|
|
3262
|
+
% self.identifier_preparer._format_sequence_string_literal(
|
|
3263
|
+
_CompilerSequence(seq_name, effective_schema)
|
|
3201
3264
|
)
|
|
3202
|
-
|
|
3203
|
-
exc = "select nextval('\"%s\"')" % (seq_name,)
|
|
3265
|
+
)
|
|
3204
3266
|
|
|
3205
3267
|
return self._execute_scalar(exc, column.type)
|
|
3206
3268
|
|
|
@@ -3692,7 +3754,11 @@ class PGDialect(default.DefaultDialect):
|
|
|
3692
3754
|
def get_schema_names(self, connection, **kw):
|
|
3693
3755
|
query = (
|
|
3694
3756
|
select(pg_catalog.pg_namespace.c.nspname)
|
|
3695
|
-
.where(
|
|
3757
|
+
.where(
|
|
3758
|
+
~pg_catalog.pg_namespace.c.nspname.startswith(
|
|
3759
|
+
"pg_", autoescape=True
|
|
3760
|
+
)
|
|
3761
|
+
)
|
|
3696
3762
|
.order_by(pg_catalog.pg_namespace.c.nspname)
|
|
3697
3763
|
)
|
|
3698
3764
|
return connection.scalars(query).all()
|
|
@@ -5229,9 +5295,7 @@ class PGDialect(default.DefaultDialect):
|
|
|
5229
5295
|
util.warn("Could not parse CHECK constraint text: %r" % src)
|
|
5230
5296
|
sqltext = ""
|
|
5231
5297
|
else:
|
|
5232
|
-
sqltext =
|
|
5233
|
-
r"^[\s\n]*\((.+)\)[\s\n]*$", flags=re.DOTALL
|
|
5234
|
-
).sub(r"\1", m.group(1))
|
|
5298
|
+
sqltext = util.strip_outer_parens(m.group(1))
|
|
5235
5299
|
entry = {
|
|
5236
5300
|
"name": check_name,
|
|
5237
5301
|
"sqltext": sqltext,
|
|
@@ -49,6 +49,9 @@ class JSONPathType(sqltypes.JSON.JSONPathType):
|
|
|
49
49
|
if isinstance(value, str):
|
|
50
50
|
# If it's already a string assume that it's in json path
|
|
51
51
|
# format. This allows using cast with json paths literals
|
|
52
|
+
# Still need to process through super_proc for proper escaping
|
|
53
|
+
if super_proc:
|
|
54
|
+
value = super_proc(value)
|
|
52
55
|
return value
|
|
53
56
|
elif value:
|
|
54
57
|
# If it's already a string assume that it's in json path
|
|
@@ -76,6 +76,36 @@ based on the kind of SQLite database that's requested:
|
|
|
76
76
|
may be used by specifying it via the
|
|
77
77
|
:paramref:`_sa.create_engine.poolclass` parameter.
|
|
78
78
|
|
|
79
|
+
.. _aiosqlite_memory:
|
|
80
|
+
|
|
81
|
+
Using a Memory Database with Multiple Coroutines
|
|
82
|
+
-------------------------------------------------
|
|
83
|
+
|
|
84
|
+
The default :class:`.StaticPool` used for ``:memory:`` databases forces all
|
|
85
|
+
coroutines to share a single DBAPI connection. Because SQLite maintains only
|
|
86
|
+
one transaction state per connection, concurrent coroutines can interfere
|
|
87
|
+
with each other — a ``ROLLBACK`` in one coroutine will also discard
|
|
88
|
+
uncommitted work from any other coroutine using the same engine.
|
|
89
|
+
|
|
90
|
+
For async workloads where multiple :class:`.AsyncSession` or
|
|
91
|
+
:class:`.AsyncConnection` objects may be active simultaneously, use SQLite's
|
|
92
|
+
shared-cache URI mode instead. This gives each checkout its own DBAPI
|
|
93
|
+
connection with independent transaction state while still sharing one
|
|
94
|
+
in-memory database::
|
|
95
|
+
|
|
96
|
+
engine = create_async_engine(
|
|
97
|
+
"sqlite+aiosqlite:///file::memory:?cache=shared&uri=true"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
Because this URL form is treated as a file-based database by the dialect,
|
|
101
|
+
:class:`.AsyncAdaptedQueuePool` is used automatically and no additional
|
|
102
|
+
configuration is needed.
|
|
103
|
+
|
|
104
|
+
See the pysqlite documentation at
|
|
105
|
+
:ref:`pysqlite_uri_shared_cache` for full details on shared-cache memory
|
|
106
|
+
databases, including how to use named databases to maintain multiple
|
|
107
|
+
independent in-memory databases within the same process.
|
|
108
|
+
|
|
79
109
|
""" # noqa
|
|
80
110
|
|
|
81
111
|
from __future__ import annotations
|
|
@@ -949,6 +949,15 @@ dialect in conjunction with the :class:`_schema.Table` construct:
|
|
|
949
949
|
|
|
950
950
|
.. versionadded:: 2.0.37
|
|
951
951
|
|
|
952
|
+
Both options are also reflected, so that a :class:`_schema.Table` which is
|
|
953
|
+
autoloaded from a database that was created using either keyword will render
|
|
954
|
+
that keyword again when the table is recreated. The reflected values are
|
|
955
|
+
also available directly from
|
|
956
|
+
:meth:`_engine.Inspector.get_table_options`.
|
|
957
|
+
|
|
958
|
+
.. versionadded:: 2.0.53 Added reflection support for the ``WITHOUT ROWID``
|
|
959
|
+
and ``STRICT`` table options.
|
|
960
|
+
|
|
952
961
|
.. seealso::
|
|
953
962
|
|
|
954
963
|
`SQLite CREATE TABLE options
|
|
@@ -1128,7 +1137,7 @@ class DATETIME(_DateTimeMixin, sqltypes.DateTime):
|
|
|
1128
1137
|
storage_format=(
|
|
1129
1138
|
"%(year)04d/%(month)02d/%(day)02d %(hour)02d:%(minute)02d:%(second)02d"
|
|
1130
1139
|
),
|
|
1131
|
-
regexp=r"(\d+)/(\d+)/(\d+) (\d+)
|
|
1140
|
+
regexp=r"(\d+)/(\d+)/(\d+) (\d+):(\d+):(\d+)",
|
|
1132
1141
|
)
|
|
1133
1142
|
|
|
1134
1143
|
:param truncate_microseconds: when ``True`` microseconds will be truncated
|
|
@@ -2067,6 +2076,37 @@ class SQLiteExecutionContext(default.DefaultExecutionContext):
|
|
|
2067
2076
|
return colname, None
|
|
2068
2077
|
|
|
2069
2078
|
|
|
2079
|
+
# regexp that locates a FOREIGN KEY clause within the verbatim CREATE TABLE
|
|
2080
|
+
# text that sqlite stores in sqlite_master. the referred-columns group
|
|
2081
|
+
# requires a non-empty separator between column tokens; an earlier form made
|
|
2082
|
+
# the separator optional, which turned the repeat into a nested quantifier and
|
|
2083
|
+
# let a long word run backtrack exponentially. kept at module level so it can
|
|
2084
|
+
# be exercised directly from the tests.
|
|
2085
|
+
FK_PATTERN = re.compile(
|
|
2086
|
+
r'(?:CONSTRAINT\s+(?:"(.+?)"|(\w+))\s+)?'
|
|
2087
|
+
r"FOREIGN\s+KEY\s*\(\s*(.+?)\s*\)\s+"
|
|
2088
|
+
r'REFERENCES\s+(?:(?:"(.+?)")|([a-z0-9_]+))\s*\(\s*((?:"[^"]+"|[a-z0-9_]+)(?:(?:\s*,\s*|\s+)(?:"[^"]+"|[a-z0-9_]+))*\s*)\)\s*' # noqa: E501
|
|
2089
|
+
r"((?:ON\s+(?:DELETE|UPDATE)\s+"
|
|
2090
|
+
r"(?:SET\s+NULL|SET\s+DEFAULT|CASCADE|RESTRICT|"
|
|
2091
|
+
r"NO\s+ACTION)\s*)*)"
|
|
2092
|
+
r"((?:NOT\s+)?DEFERRABLE)?"
|
|
2093
|
+
r"(?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))?",
|
|
2094
|
+
re.I,
|
|
2095
|
+
)
|
|
2096
|
+
|
|
2097
|
+
# regexp that locates the table option keywords which may trail the closing
|
|
2098
|
+
# paren of the column list in the verbatim CREATE TABLE text. the match is
|
|
2099
|
+
# anchored at the end of the statement, which makes the closing paren
|
|
2100
|
+
# unambiguous, as neither keyword can itself contain one. the keywords may
|
|
2101
|
+
# be given in either order; as each may appear only once, the repeated group
|
|
2102
|
+
# leaves one named group per keyword holding the text that was matched.
|
|
2103
|
+
TABLE_OPTIONS_PATTERN = re.compile(
|
|
2104
|
+
r"\)(?:\s*,?\s*(?:(?P<without_rowid>WITHOUT\s+ROWID)"
|
|
2105
|
+
r"|(?P<strict>STRICT)))*\s*$",
|
|
2106
|
+
re.I,
|
|
2107
|
+
)
|
|
2108
|
+
|
|
2109
|
+
|
|
2070
2110
|
class SQLiteDialect(default.DefaultDialect):
|
|
2071
2111
|
name = "sqlite"
|
|
2072
2112
|
supports_alter = False
|
|
@@ -2381,6 +2421,29 @@ class SQLiteDialect(default.DefaultDialect):
|
|
|
2381
2421
|
f"{schema}.{view_name}" if schema else view_name
|
|
2382
2422
|
)
|
|
2383
2423
|
|
|
2424
|
+
@reflection.cache
|
|
2425
|
+
def get_table_options(self, connection, table_name, schema=None, **kw):
|
|
2426
|
+
tablesql = self._get_table_sql(
|
|
2427
|
+
connection, table_name, schema=schema, **kw
|
|
2428
|
+
)
|
|
2429
|
+
|
|
2430
|
+
options = {}
|
|
2431
|
+
|
|
2432
|
+
# tablesql is None for the internal sqlite_ tables, which have no
|
|
2433
|
+
# entry in sqlite_master
|
|
2434
|
+
if tablesql is not None:
|
|
2435
|
+
match = TABLE_OPTIONS_PATTERN.search(tablesql.strip())
|
|
2436
|
+
if match:
|
|
2437
|
+
if match.group("without_rowid"):
|
|
2438
|
+
options["sqlite_with_rowid"] = False
|
|
2439
|
+
if match.group("strict"):
|
|
2440
|
+
options["sqlite_strict"] = True
|
|
2441
|
+
|
|
2442
|
+
if options:
|
|
2443
|
+
return options
|
|
2444
|
+
else:
|
|
2445
|
+
return ReflectionDefaults.table_options()
|
|
2446
|
+
|
|
2384
2447
|
@reflection.cache
|
|
2385
2448
|
def get_columns(self, connection, table_name, schema=None, **kw):
|
|
2386
2449
|
pragma = "table_info"
|
|
@@ -2549,7 +2612,7 @@ class SQLiteDialect(default.DefaultDialect):
|
|
|
2549
2612
|
constraint_name = None
|
|
2550
2613
|
table_data = self._get_table_sql(connection, table_name, schema=schema)
|
|
2551
2614
|
if table_data:
|
|
2552
|
-
PK_PATTERN = r'CONSTRAINT
|
|
2615
|
+
PK_PATTERN = r'CONSTRAINT\s+(?:"(.+?)"|(\w+))\s+PRIMARY\s+KEY'
|
|
2553
2616
|
result = re.search(PK_PATTERN, table_data, re.I)
|
|
2554
2617
|
if result:
|
|
2555
2618
|
constraint_name = result.group(1) or result.group(2)
|
|
@@ -2653,16 +2716,7 @@ class SQLiteDialect(default.DefaultDialect):
|
|
|
2653
2716
|
# FKs, namely the name of the constraint and other options.
|
|
2654
2717
|
# so parsing the columns is really about matching it up to what
|
|
2655
2718
|
# we already have.
|
|
2656
|
-
|
|
2657
|
-
r'(?:CONSTRAINT +(?:"(.+?)"|(\w+)) +)?'
|
|
2658
|
-
r"FOREIGN KEY *\( *(.+?) *\) +"
|
|
2659
|
-
r'REFERENCES +(?:(?:"(.+?)")|([a-z0-9_]+)) *\( *((?:(?:"[^"]+"|[a-z0-9_]+) *(?:, *)?)+)\) *' # noqa: E501
|
|
2660
|
-
r"((?:ON (?:DELETE|UPDATE) "
|
|
2661
|
-
r"(?:SET NULL|SET DEFAULT|CASCADE|RESTRICT|NO ACTION) *)*)"
|
|
2662
|
-
r"((?:NOT +)?DEFERRABLE)?"
|
|
2663
|
-
r"(?: +INITIALLY +(DEFERRED|IMMEDIATE))?"
|
|
2664
|
-
)
|
|
2665
|
-
for match in re.finditer(FK_PATTERN, table_data, re.I):
|
|
2719
|
+
for match in FK_PATTERN.finditer(table_data):
|
|
2666
2720
|
(
|
|
2667
2721
|
constraint_quoted_name,
|
|
2668
2722
|
constraint_name,
|
|
@@ -2687,7 +2741,13 @@ class SQLiteDialect(default.DefaultDialect):
|
|
|
2687
2741
|
referred_name = referred_quoted_name or referred_name
|
|
2688
2742
|
options = {}
|
|
2689
2743
|
|
|
2690
|
-
|
|
2744
|
+
# a newline may separate the words of an
|
|
2745
|
+
# ON DELETE / ON UPDATE clause; normalize to single
|
|
2746
|
+
# spaces so the tokens below compare correctly
|
|
2747
|
+
onupdatedelete = re.sub(
|
|
2748
|
+
r"\s+", " ", onupdatedelete.upper()
|
|
2749
|
+
).strip()
|
|
2750
|
+
for token in re.split(r" *\bON\b *", onupdatedelete):
|
|
2691
2751
|
if token.startswith("DELETE"):
|
|
2692
2752
|
ondelete = token[6:].strip()
|
|
2693
2753
|
if ondelete and ondelete != "NO ACTION":
|
|
@@ -2770,11 +2830,11 @@ class SQLiteDialect(default.DefaultDialect):
|
|
|
2770
2830
|
if table_data is None:
|
|
2771
2831
|
return
|
|
2772
2832
|
UNIQUE_PATTERN = (
|
|
2773
|
-
r'(?:CONSTRAINT
|
|
2833
|
+
r'(?:CONSTRAINT\s+(?:"(.+?)"|(\w+))\s+)?UNIQUE\s*\((.+?)\)'
|
|
2774
2834
|
)
|
|
2775
2835
|
INLINE_UNIQUE_PATTERN = (
|
|
2776
2836
|
r'(?:(".+?")|(?:[\[`])?([a-z0-9_]+)(?:[\]`])?)[\t ]'
|
|
2777
|
-
r"+[a-z0-9_ ]
|
|
2837
|
+
r"+[a-z0-9_]+(?:[\t ]+[a-z0-9_]+)*?[\t ]+UNIQUE"
|
|
2778
2838
|
)
|
|
2779
2839
|
|
|
2780
2840
|
for match in re.finditer(UNIQUE_PATTERN, table_data, re.I):
|
|
@@ -2865,32 +2925,14 @@ class SQLiteDialect(default.DefaultDialect):
|
|
|
2865
2925
|
flags=re.DOTALL,
|
|
2866
2926
|
)
|
|
2867
2927
|
|
|
2868
|
-
# Find the matching closing parenthesis
|
|
2869
|
-
#
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
# Track string literal context
|
|
2877
|
-
if char == "'" and not in_double_quote:
|
|
2878
|
-
in_single_quote = not in_single_quote
|
|
2879
|
-
elif char == '"' and not in_single_quote:
|
|
2880
|
-
in_double_quote = not in_double_quote
|
|
2881
|
-
# Only count parens when not inside a string literal
|
|
2882
|
-
elif not in_single_quote and not in_double_quote:
|
|
2883
|
-
if char == "(":
|
|
2884
|
-
paren_count += 1
|
|
2885
|
-
elif char == ")":
|
|
2886
|
-
paren_count -= 1
|
|
2887
|
-
if paren_count == 0:
|
|
2888
|
-
# Successfully found matching closing parenthesis
|
|
2889
|
-
sqltext = table_data[start:pos].strip()
|
|
2890
|
-
cks.append(
|
|
2891
|
-
{"sqltext": sqltext, "name": constraint_name}
|
|
2892
|
-
)
|
|
2893
|
-
break
|
|
2928
|
+
# Find the matching closing parenthesis with quote-aware paren
|
|
2929
|
+
# counting. ``match.end() - 1`` is the position of the ``(``
|
|
2930
|
+
# that opened the CHECK clause; ``match.end()`` is the first
|
|
2931
|
+
# character of the constraint body.
|
|
2932
|
+
close = util.find_matching_paren(table_data, match.end() - 1)
|
|
2933
|
+
if close is not None:
|
|
2934
|
+
sqltext = table_data[match.end() : close].strip()
|
|
2935
|
+
cks.append({"sqltext": sqltext, "name": constraint_name})
|
|
2894
2936
|
|
|
2895
2937
|
cks.sort(key=lambda d: d["name"] or "~") # sort None as last
|
|
2896
2938
|
if cks:
|
|
@@ -209,8 +209,8 @@ processing.
|
|
|
209
209
|
|
|
210
210
|
.. _pysqlite_threading_pooling:
|
|
211
211
|
|
|
212
|
-
Threading/Pooling Behavior
|
|
213
|
-
|
|
212
|
+
Concurrency/Threading/Pooling Behavior
|
|
213
|
+
--------------------------------------
|
|
214
214
|
|
|
215
215
|
The ``sqlite3`` DBAPI by default prohibits the use of a particular connection
|
|
216
216
|
in a thread which is not the one in which it was created. As SQLite has
|
|
@@ -261,15 +261,82 @@ connection reuse implemented by :class:`.QueuePool`. However, it still
|
|
|
261
261
|
may be beneficial to use this class if the application is experiencing
|
|
262
262
|
issues with files being locked.
|
|
263
263
|
|
|
264
|
-
Using a Memory Database in Multiple Threads
|
|
265
|
-
|
|
264
|
+
Using a Memory Database in Multiple Threads or Coroutines
|
|
265
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
266
|
+
|
|
267
|
+
A ``:memory:`` SQLite database exists only within the scope of a single
|
|
268
|
+
DBAPI connection. It is not possible for two separate ``sqlite3``
|
|
269
|
+
connection objects to access the same ``:memory:`` database unless SQLite's
|
|
270
|
+
shared-cache feature is enabled. Without shared cache, each connection
|
|
271
|
+
creates its own independent in-memory database.
|
|
272
|
+
|
|
273
|
+
This means a ``:memory:`` database is **not suitable** for use with
|
|
274
|
+
multiple concurrent threads or coroutines unless either:
|
|
275
|
+
|
|
276
|
+
* All workers are fully serialized (mutexed) against each other
|
|
277
|
+
such that only one is using the database at a time, or
|
|
278
|
+
* SQLite's shared-cache URI feature is used to allow multiple
|
|
279
|
+
independent connections to access the same in-memory database.
|
|
280
|
+
|
|
281
|
+
The same considerations apply when using the :ref:`aiosqlite <aiosqlite>`
|
|
282
|
+
dialect, which wraps ``pysqlite`` connections in an async interface —
|
|
283
|
+
without shared cache, each connection still creates its own independent
|
|
284
|
+
in-memory database.
|
|
285
|
+
|
|
286
|
+
The recommended approach for multithreaded or async in-memory
|
|
287
|
+
use is the shared-cache URI feature, described below.
|
|
288
|
+
|
|
289
|
+
.. _pysqlite_uri_shared_cache:
|
|
290
|
+
|
|
291
|
+
Using a Shared-Cache Memory Database
|
|
292
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
293
|
+
|
|
294
|
+
SQLite's
|
|
295
|
+
`URI shared-cache <https://www.sqlite.org/sharedcache.html>`_
|
|
296
|
+
mode allows multiple independent DBAPI connections, each with
|
|
297
|
+
their own transaction state, to access the same in-memory
|
|
298
|
+
database. This is enabled by using a ``file:`` URI with
|
|
299
|
+
``cache=shared`` and passing ``uri=true`` in the query
|
|
300
|
+
string::
|
|
301
|
+
|
|
302
|
+
engine = create_engine("sqlite:///file::memory:?cache=shared&uri=true")
|
|
303
|
+
|
|
304
|
+
For async use with aiosqlite, use
|
|
305
|
+
:func:`_asyncio.create_async_engine`::
|
|
306
|
+
|
|
307
|
+
engine = create_async_engine(
|
|
308
|
+
"sqlite+aiosqlite:///file::memory:?cache=shared&uri=true"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
Because this URL form is treated as a file-based database by the
|
|
312
|
+
dialect, :class:`.QueuePool` is used automatically and
|
|
313
|
+
``check_same_thread`` defaults to ``False``, so no additional pool
|
|
314
|
+
or connect_args configuration is needed. Each checkout from the
|
|
315
|
+
pool is a distinct DBAPI connection with its own transaction state,
|
|
316
|
+
and the in-memory database persists as long as at least one
|
|
317
|
+
connection remains open.
|
|
318
|
+
|
|
319
|
+
The shared-cache database is scoped by the filename component of
|
|
320
|
+
the URI. ``file::memory:`` (empty name) is process-global — all
|
|
321
|
+
engines in the process that use this URI share the same database.
|
|
322
|
+
To maintain multiple independent in-memory databases within the
|
|
323
|
+
same process, supply a distinct name for each::
|
|
324
|
+
|
|
325
|
+
engine_a = create_engine(
|
|
326
|
+
"sqlite:///file:db_a?mode=memory&cache=shared&uri=true"
|
|
327
|
+
)
|
|
328
|
+
engine_b = create_engine(
|
|
329
|
+
"sqlite:///file:db_b?mode=memory&cache=shared&uri=true"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
Using StaticPool for Single-Connection Memory Databases
|
|
333
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
266
334
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
as ``False``::
|
|
335
|
+
An older approach for sharing a ``:memory:`` database among threads
|
|
336
|
+
is to force all checkouts to return the same DBAPI connection using
|
|
337
|
+
:class:`.StaticPool`. **This approach does not support any form of
|
|
338
|
+
concurrency** and is only useful when all access to the engine is
|
|
339
|
+
fully serialized, such as in single-threaded test suites::
|
|
273
340
|
|
|
274
341
|
from sqlalchemy.pool import StaticPool
|
|
275
342
|
|
|
@@ -279,8 +346,18 @@ as ``False``::
|
|
|
279
346
|
poolclass=StaticPool,
|
|
280
347
|
)
|
|
281
348
|
|
|
282
|
-
|
|
283
|
-
|
|
349
|
+
.. warning::
|
|
350
|
+
|
|
351
|
+
Because :class:`.StaticPool` maintains a single DBAPI connection,
|
|
352
|
+
all :class:`.Session` or :class:`.Connection` objects that use
|
|
353
|
+
this engine share that one underlying connection and its single
|
|
354
|
+
SQLite transaction state. A ``ROLLBACK`` issued by one session
|
|
355
|
+
(e.g. during error handling) will also roll back uncommitted work
|
|
356
|
+
from any other session, and concurrent ``COMMIT`` / ``ROLLBACK``
|
|
357
|
+
calls can interfere with each other unpredictably. This approach
|
|
358
|
+
is only appropriate when access to the engine is fully serialized,
|
|
359
|
+
such as in single-threaded test suites. For concurrent workloads,
|
|
360
|
+
use the shared-cache URI approach described above.
|
|
284
361
|
|
|
285
362
|
Using Temporary Tables with SQLite
|
|
286
363
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|