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.
Files changed (54) hide show
  1. sqlalchemy/__init__.py +1 -1
  2. sqlalchemy/connectors/pyodbc.py +13 -4
  3. sqlalchemy/dialects/mssql/base.py +11 -3
  4. sqlalchemy/dialects/mysql/base.py +2 -2
  5. sqlalchemy/dialects/oracle/oracledb.py +21 -1
  6. sqlalchemy/dialects/postgresql/base.py +77 -13
  7. sqlalchemy/dialects/postgresql/json.py +3 -0
  8. sqlalchemy/dialects/sqlite/aiosqlite.py +30 -0
  9. sqlalchemy/dialects/sqlite/base.py +83 -41
  10. sqlalchemy/dialects/sqlite/pysqlite.py +89 -12
  11. sqlalchemy/engine/reflection.py +9 -0
  12. sqlalchemy/ext/asyncio/engine.py +6 -6
  13. sqlalchemy/ext/asyncio/scoping.py +12 -9
  14. sqlalchemy/ext/asyncio/session.py +6 -5
  15. sqlalchemy/orm/bulk_persistence.py +16 -0
  16. sqlalchemy/orm/decl_base.py +14 -0
  17. sqlalchemy/orm/path_registry.py +55 -13
  18. sqlalchemy/orm/properties.py +2 -2
  19. sqlalchemy/orm/relationships.py +13 -6
  20. sqlalchemy/orm/scoping.py +7 -5
  21. sqlalchemy/orm/session.py +25 -23
  22. sqlalchemy/orm/strategies.py +10 -3
  23. sqlalchemy/orm/strategy_options.py +11 -6
  24. sqlalchemy/orm/util.py +73 -4
  25. sqlalchemy/pool/base.py +29 -11
  26. sqlalchemy/sql/_elements_constructors.py +12 -0
  27. sqlalchemy/sql/compiler.py +27 -1
  28. sqlalchemy/sql/elements.py +7 -2
  29. sqlalchemy/sql/schema.py +16 -17
  30. sqlalchemy/sql/selectable.py +19 -9
  31. sqlalchemy/sql/sqltypes.py +9 -5
  32. sqlalchemy/testing/__init__.py +2 -0
  33. sqlalchemy/testing/cancellation.py +237 -0
  34. sqlalchemy/testing/config.py +7 -0
  35. sqlalchemy/testing/fixtures/mypy.py +5 -3
  36. sqlalchemy/testing/fixtures/orm.py +2 -2
  37. sqlalchemy/testing/fixtures/sql.py +2 -2
  38. sqlalchemy/testing/plugin/plugin_base.py +4 -0
  39. sqlalchemy/testing/plugin/pytestplugin.py +30 -0
  40. sqlalchemy/testing/requirements.py +23 -0
  41. sqlalchemy/testing/suite/test_dialect.py +1 -0
  42. sqlalchemy/testing/suite/test_reflection.py +138 -0
  43. sqlalchemy/testing/suite/test_update_delete.py +37 -0
  44. sqlalchemy/testing/util.py +41 -0
  45. sqlalchemy/util/__init__.py +2 -0
  46. sqlalchemy/util/compat.py +1 -1
  47. sqlalchemy/util/langhelpers.py +105 -2
  48. sqlalchemy/util/typing.py +2 -0
  49. {sqlalchemy-2.0.51.dist-info → sqlalchemy-2.0.53.dist-info}/METADATA +5 -2
  50. {sqlalchemy-2.0.51.dist-info → sqlalchemy-2.0.53.dist-info}/RECORD +54 -52
  51. {sqlalchemy-2.0.51.dist-info → sqlalchemy-2.0.53.dist-info}/WHEEL +1 -1
  52. sqlalchemy-2.0.53.dist-info/licenses/AUTHORS +30 -0
  53. {sqlalchemy-2.0.51.dist-info → sqlalchemy-2.0.53.dist-info}/licenses/LICENSE +0 -0
  54. {sqlalchemy-2.0.51.dist-info → sqlalchemy-2.0.53.dist-info}/top_level.txt +0 -0
sqlalchemy/__init__.py CHANGED
@@ -269,7 +269,7 @@ from .types import Uuid as Uuid
269
269
  from .types import VARBINARY as VARBINARY
270
270
  from .types import VARCHAR as VARCHAR
271
271
 
272
- __version__ = "2.0.51"
272
+ __version__ = "2.0.53"
273
273
 
274
274
 
275
275
  def __go(lcls: Any) -> None:
@@ -77,10 +77,16 @@ class PyODBCConnector(Connector):
77
77
  else:
78
78
 
79
79
  def check_quote(token: str) -> str:
80
- if ";" in str(token) or str(token).startswith("{"):
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("DRIVER={%s}" % driver)
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(["%s=%s" % (k, v) for k, v in keys.items()])
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 = set(opts).difference(partition_options)
2109
- part_options = set(opts).intersection(partition_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(AsyncAdapt_dbapi_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('%s')" % self.preparer.format_sequence(seq)
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('%s')"
3160
- % self.identifier_preparer.format_sequence(seq)
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
- if effective_schema is not None:
3198
- exc = 'select nextval(\'"%s"."%s"\')' % (
3199
- effective_schema,
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
- else:
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(pg_catalog.pg_namespace.c.nspname.not_like("pg_%"))
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 = re.compile(
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+)-(\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 +(?:"(.+?)"|(\w+)) +PRIMARY KEY'
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
- FK_PATTERN = (
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
- for token in re.split(r" *\bON\b *", onupdatedelete.upper()):
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 +(?:"(.+?)"|(\w+)) +)?UNIQUE *\((.+?)\)'
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_ ]+?[\t ]+UNIQUE"
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 by counting balanced parens
2869
- # Must track string context to ignore parens inside string literals
2870
- start = match.end() # Position after 'CHECK ('
2871
- paren_count = 1
2872
- in_single_quote = False
2873
- in_double_quote = False
2874
-
2875
- for pos, char in enumerate(table_data[start:], start):
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
- To use a ``:memory:`` database in a multithreaded scenario, the same
268
- connection object must be shared among threads, since the database exists
269
- only within the scope of that connection. The
270
- :class:`.StaticPool` implementation will maintain a single connection
271
- globally, and the ``check_same_thread`` flag can be passed to Pysqlite
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
- Note that using a ``:memory:`` database in multiple threads requires a recent
283
- version of SQLite.
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
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^