sqlspec 0.55.0__py3-none-any.whl → 0.56.0__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 (129) hide show
  1. sqlspec/adapters/adbc/adk/store.py +8 -16
  2. sqlspec/adapters/adbc/core.py +182 -3
  3. sqlspec/adapters/adbc/driver.py +19 -8
  4. sqlspec/adapters/adbc/litestar/store.py +72 -68
  5. sqlspec/adapters/aiomysql/adk/store.py +8 -5
  6. sqlspec/adapters/aiomysql/core.py +107 -106
  7. sqlspec/adapters/aiomysql/events/store.py +19 -23
  8. sqlspec/adapters/aiomysql/litestar/store.py +70 -30
  9. sqlspec/adapters/aiosqlite/adk/store.py +62 -63
  10. sqlspec/adapters/aiosqlite/config.py +146 -110
  11. sqlspec/adapters/aiosqlite/core.py +216 -217
  12. sqlspec/adapters/aiosqlite/driver.py +172 -172
  13. sqlspec/adapters/aiosqlite/events/store.py +16 -2
  14. sqlspec/adapters/aiosqlite/litestar/store.py +73 -58
  15. sqlspec/adapters/arrow_odbc/adk/store.py +131 -134
  16. sqlspec/adapters/arrow_odbc/core.py +13 -12
  17. sqlspec/adapters/arrow_odbc/driver.py +13 -13
  18. sqlspec/adapters/arrow_odbc/litestar/store.py +4 -0
  19. sqlspec/adapters/asyncmy/adk/store.py +8 -5
  20. sqlspec/adapters/asyncmy/core.py +107 -106
  21. sqlspec/adapters/asyncmy/events/store.py +19 -23
  22. sqlspec/adapters/asyncmy/litestar/store.py +70 -30
  23. sqlspec/adapters/asyncpg/adk/store.py +90 -50
  24. sqlspec/adapters/asyncpg/core.py +106 -107
  25. sqlspec/adapters/asyncpg/driver.py +88 -88
  26. sqlspec/adapters/asyncpg/events/store.py +32 -0
  27. sqlspec/adapters/asyncpg/litestar/store.py +59 -33
  28. sqlspec/adapters/bigquery/adk/store.py +4 -9
  29. sqlspec/adapters/bigquery/core.py +374 -375
  30. sqlspec/adapters/bigquery/driver.py +108 -108
  31. sqlspec/adapters/bigquery/events/store.py +58 -16
  32. sqlspec/adapters/bigquery/litestar/store.py +113 -69
  33. sqlspec/adapters/cockroach_asyncpg/adk/store.py +8 -8
  34. sqlspec/adapters/cockroach_asyncpg/driver.py +19 -19
  35. sqlspec/adapters/cockroach_asyncpg/litestar/store.py +47 -19
  36. sqlspec/adapters/cockroach_psycopg/adk/store.py +16 -15
  37. sqlspec/adapters/cockroach_psycopg/driver.py +36 -36
  38. sqlspec/adapters/cockroach_psycopg/litestar/store.py +83 -44
  39. sqlspec/adapters/duckdb/adk/store.py +8 -9
  40. sqlspec/adapters/duckdb/core.py +7 -8
  41. sqlspec/adapters/duckdb/driver.py +4 -4
  42. sqlspec/adapters/duckdb/litestar/store.py +72 -68
  43. sqlspec/adapters/mssql_python/adk/store.py +42 -35
  44. sqlspec/adapters/mssql_python/core.py +25 -6
  45. sqlspec/adapters/mssql_python/driver.py +31 -13
  46. sqlspec/adapters/mssql_python/litestar/store.py +36 -32
  47. sqlspec/adapters/mysqlconnector/adk/store.py +16 -10
  48. sqlspec/adapters/mysqlconnector/core.py +86 -87
  49. sqlspec/adapters/mysqlconnector/events/store.py +38 -35
  50. sqlspec/adapters/mysqlconnector/litestar/store.py +93 -45
  51. sqlspec/adapters/oracledb/_json_handlers.py +65 -60
  52. sqlspec/adapters/oracledb/_storage.py +215 -0
  53. sqlspec/adapters/oracledb/adk/store.py +183 -358
  54. sqlspec/adapters/oracledb/config.py +142 -124
  55. sqlspec/adapters/oracledb/core.py +212 -213
  56. sqlspec/adapters/oracledb/data_dictionary.py +495 -309
  57. sqlspec/adapters/oracledb/driver.py +49 -60
  58. sqlspec/adapters/oracledb/events/store.py +239 -238
  59. sqlspec/adapters/oracledb/litestar/store.py +223 -172
  60. sqlspec/adapters/psqlpy/adk/store.py +52 -12
  61. sqlspec/adapters/psqlpy/core.py +244 -211
  62. sqlspec/adapters/psqlpy/driver.py +31 -15
  63. sqlspec/adapters/psqlpy/events/store.py +32 -0
  64. sqlspec/adapters/psqlpy/litestar/store.py +59 -33
  65. sqlspec/adapters/psycopg/adk/store.py +72 -23
  66. sqlspec/adapters/psycopg/core.py +54 -55
  67. sqlspec/adapters/psycopg/driver.py +294 -250
  68. sqlspec/adapters/psycopg/events/store.py +41 -0
  69. sqlspec/adapters/psycopg/litestar/store.py +138 -102
  70. sqlspec/adapters/pymssql/adk/store.py +65 -57
  71. sqlspec/adapters/pymssql/core.py +36 -19
  72. sqlspec/adapters/pymssql/driver.py +4 -4
  73. sqlspec/adapters/pymssql/litestar/store.py +36 -32
  74. sqlspec/adapters/pymysql/adk/store.py +8 -5
  75. sqlspec/adapters/pymysql/core.py +86 -87
  76. sqlspec/adapters/pymysql/events/store.py +15 -11
  77. sqlspec/adapters/pymysql/litestar/store.py +67 -27
  78. sqlspec/adapters/spanner/adk/store.py +8 -8
  79. sqlspec/adapters/spanner/core.py +28 -29
  80. sqlspec/adapters/spanner/driver.py +224 -224
  81. sqlspec/adapters/spanner/litestar/store.py +73 -64
  82. sqlspec/adapters/sqlite/adk/store.py +62 -63
  83. sqlspec/adapters/sqlite/config.py +129 -93
  84. sqlspec/adapters/sqlite/core.py +170 -171
  85. sqlspec/adapters/sqlite/driver.py +178 -178
  86. sqlspec/adapters/sqlite/events/store.py +16 -2
  87. sqlspec/adapters/sqlite/litestar/store.py +85 -70
  88. sqlspec/builder/_base.py +60 -58
  89. sqlspec/builder/_dml.py +1 -1
  90. sqlspec/config.py +335 -234
  91. sqlspec/core/hashing.py +34 -17
  92. sqlspec/data_dictionary/dialects/oracle.py +35 -0
  93. sqlspec/data_dictionary/sql/oracle/version.sql +6 -0
  94. sqlspec/data_dictionary/sql/spanner/change_streams/googlesql/list.sql +2 -2
  95. sqlspec/data_dictionary/sql/spanner/change_streams/googlesql/tables.sql +2 -2
  96. sqlspec/data_dictionary/sql/spanner/columns/googlesql/by_schema.sql +2 -2
  97. sqlspec/data_dictionary/sql/spanner/columns.sql +2 -2
  98. sqlspec/data_dictionary/sql/spanner/constraints/googlesql/by_schema.sql +2 -2
  99. sqlspec/data_dictionary/sql/spanner/foreign_keys.sql +5 -5
  100. sqlspec/data_dictionary/sql/spanner/indexes/googlesql/by_schema.sql +2 -2
  101. sqlspec/data_dictionary/sql/spanner/indexes.sql +2 -2
  102. sqlspec/data_dictionary/sql/spanner/privileges/googlesql/by_schema.sql +2 -2
  103. sqlspec/data_dictionary/sql/spanner/property_graphs/googlesql/list.sql +2 -2
  104. sqlspec/data_dictionary/sql/spanner/routines/googlesql/by_schema.sql +2 -2
  105. sqlspec/data_dictionary/sql/spanner/routines/googlesql/parameters_by_schema.sql +2 -2
  106. sqlspec/data_dictionary/sql/spanner/sequences/googlesql/by_schema.sql +2 -2
  107. sqlspec/data_dictionary/sql/spanner/system/googlesql/query_stats_top.sql +1 -1
  108. sqlspec/data_dictionary/sql/spanner/system/googlesql/table_sizes.sql +2 -2
  109. sqlspec/data_dictionary/sql/spanner/tables/googlesql/by_schema.sql +2 -2
  110. sqlspec/data_dictionary/sql/spanner/tables.sql +1 -1
  111. sqlspec/data_dictionary/sql/spanner/views/googlesql/by_schema.sql +2 -2
  112. sqlspec/extensions/adk/_config_utils.py +27 -2
  113. sqlspec/extensions/adk/memory/store.py +64 -3
  114. sqlspec/extensions/adk/migrations/0002_reset_adk_tables.py +15 -1
  115. sqlspec/extensions/adk/store.py +86 -16
  116. sqlspec/extensions/events/_store.py +70 -1
  117. sqlspec/extensions/events/migrations/0001_create_event_queue.py +43 -1
  118. sqlspec/extensions/litestar/config.py +44 -0
  119. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +5 -0
  120. sqlspec/extensions/litestar/store.py +108 -25
  121. sqlspec/migrations/__init__.py +5 -0
  122. sqlspec/migrations/base.py +93 -125
  123. sqlspec/migrations/schema.py +402 -0
  124. sqlspec/migrations/tracker.py +51 -118
  125. {sqlspec-0.55.0.dist-info → sqlspec-0.56.0.dist-info}/METADATA +18 -21
  126. {sqlspec-0.55.0.dist-info → sqlspec-0.56.0.dist-info}/RECORD +129 -127
  127. {sqlspec-0.55.0.dist-info → sqlspec-0.56.0.dist-info}/WHEEL +0 -0
  128. {sqlspec-0.55.0.dist-info → sqlspec-0.56.0.dist-info}/entry_points.txt +0 -0
  129. {sqlspec-0.55.0.dist-info → sqlspec-0.56.0.dist-info}/licenses/LICENSE +0 -0
@@ -72,6 +72,10 @@ class AdbcADKStore(BaseSyncADKStore["AdbcConfig"]):
72
72
 
73
73
  def create_tables(self) -> None:
74
74
  """Create tables if they don't exist."""
75
+ if not self.create_schema_enabled:
76
+ self.reconcile_schema()
77
+ return
78
+
75
79
  self._create_tables()
76
80
 
77
81
  def create_session(
@@ -496,19 +500,6 @@ class AdbcADKStore(BaseSyncADKStore["AdbcConfig"]):
496
500
  )
497
501
  """
498
502
 
499
- def _metadata_seed_sql(self) -> str:
500
- if self._dialect in {DIALECT_POSTGRESQL, DIALECT_SQLITE, DIALECT_DUCKDB}:
501
- return f"""
502
- INSERT INTO {self._metadata_table} (key, value)
503
- VALUES ('schema_version', '1')
504
- ON CONFLICT(key) DO NOTHING
505
- """
506
- return f"""
507
- INSERT INTO {self._metadata_table} (key, value)
508
- SELECT 'schema_version', '1'
509
- WHERE NOT EXISTS (SELECT 1 FROM {self._metadata_table} WHERE key = 'schema_version')
510
- """
511
-
512
503
  def _drop_app_states_table_sql(self) -> str:
513
504
  return f"DROP TABLE IF EXISTS {self._app_state_table}"
514
505
 
@@ -565,9 +556,6 @@ class AdbcADKStore(BaseSyncADKStore["AdbcConfig"]):
565
556
 
566
557
  cursor.execute(self._metadata_table_ddl())
567
558
  conn.commit()
568
-
569
- cursor.execute(self._metadata_seed_sql())
570
- conn.commit()
571
559
  finally:
572
560
  cursor.close()
573
561
 
@@ -1192,6 +1180,10 @@ class AdbcADKMemoryStore(BaseSyncADKMemoryStore["AdbcConfig"]):
1192
1180
 
1193
1181
  def create_tables(self) -> None:
1194
1182
  """Create tables if they don't exist."""
1183
+ if not self.create_schema_enabled:
1184
+ self.reconcile_schema()
1185
+ return
1186
+
1195
1187
  self._create_tables()
1196
1188
 
1197
1189
  def insert_memory_entries(self, entries: "list[MemoryRecord]", owner_id: "object | None" = None) -> int:
@@ -3,7 +3,13 @@
3
3
  import datetime
4
4
  import decimal
5
5
  from collections.abc import Sized
6
+ from functools import lru_cache
6
7
  from typing import TYPE_CHECKING, Any, Final, cast
8
+ from uuid import UUID
9
+
10
+ import sqlglot
11
+ from sqlglot import exp
12
+ from sqlglot.errors import ParseError
7
13
 
8
14
  from sqlspec.adapters.adbc.type_converter import get_adbc_type_converter
9
15
  from sqlspec.core import (
@@ -68,6 +74,7 @@ __all__ = (
68
74
  "normalize_script_rowcount",
69
75
  "prepare_parameters_with_casts",
70
76
  "prepare_postgres_parameters",
77
+ "prepare_postgres_uuid_bindings",
71
78
  "resolve_column_names",
72
79
  "resolve_dialect_from_config",
73
80
  "resolve_dialect_from_driver_path",
@@ -84,6 +91,7 @@ __all__ = (
84
91
  )
85
92
 
86
93
  COLUMN_CACHE_MAX_SIZE: int = 256
94
+ _UUID_TYPES: Final[tuple[type[Any], ...]] = tuple(build_uuid_coercions())
87
95
 
88
96
  DIALECT_PATTERNS: "dict[str, tuple[str, ...]]" = {
89
97
  "postgres": ("postgres", "postgresql"),
@@ -665,9 +673,6 @@ def build_profile() -> "DriverParameterProfile":
665
673
  )
666
674
 
667
675
 
668
- driver_profile: Final[DriverParameterProfile] = build_profile()
669
-
670
-
671
676
  def get_statement_config(detected_dialect: str) -> StatementConfig:
672
677
  """Create statement configuration for the specified dialect."""
673
678
  default_style, supported_styles = DIALECT_PARAMETER_STYLES.get(
@@ -850,6 +855,177 @@ def prepare_parameters_with_casts(
850
855
  return parameters
851
856
 
852
857
 
858
+ def prepare_postgres_uuid_bindings(
859
+ compiled_sql: str, prepared_parameters: Any, *, is_many: bool, dialect: str
860
+ ) -> "tuple[str, object]":
861
+ """Bind top-level UUID values through PostgreSQL-family ADBC drivers.
862
+
863
+ Args:
864
+ compiled_sql: SQL after normal statement compilation and cache rebinding.
865
+ prepared_parameters: Current execution's driver parameters.
866
+ is_many: Whether parameters contain an execute-many batch.
867
+ dialect: Resolved ADBC dialect name.
868
+
869
+ Returns:
870
+ Rewritten SQL and current-call parameters with UUID values normalized,
871
+ or the inputs unchanged when no placeholder needs a new UUID cast.
872
+ """
873
+ if not is_postgres_dialect(dialect):
874
+ return compiled_sql, prepared_parameters
875
+ if is_many:
876
+ uuid_ordinals = _detect_batch_uuid_ordinals(prepared_parameters)
877
+ elif isinstance(prepared_parameters, (list, tuple)):
878
+ uuid_ordinals = tuple(
879
+ index for index, value in enumerate(prepared_parameters, 1) if isinstance(value, _UUID_TYPES)
880
+ )
881
+ else:
882
+ uuid_ordinals = ()
883
+ if not uuid_ordinals:
884
+ return compiled_sql, prepared_parameters
885
+ sqlglot_dialect = "postgres" if dialect == "postgresql" else dialect
886
+ rewritten_sql, effective_ordinals = _rewrite_postgres_uuid_placeholders(
887
+ compiled_sql, tuple(sorted(uuid_ordinals)), sqlglot_dialect
888
+ )
889
+ if not effective_ordinals:
890
+ return compiled_sql, prepared_parameters
891
+ if is_many:
892
+ converted_rows = [
893
+ _convert_uuid_row(row, effective_ordinals, row_number)
894
+ for row_number, row in enumerate(prepared_parameters, 1)
895
+ ]
896
+ converted_parameters = tuple(converted_rows) if isinstance(prepared_parameters, tuple) else converted_rows
897
+ else:
898
+ converted_parameters = _convert_uuid_row(prepared_parameters, effective_ordinals, 1)
899
+ return rewritten_sql, converted_parameters
900
+
901
+
902
+ def _parameter_ordinal(parameter: exp.Parameter) -> "int | None":
903
+ value = parameter.this
904
+ if not isinstance(value, exp.Literal) or value.is_string:
905
+ return None
906
+ try:
907
+ return int(value.this)
908
+ except (TypeError, ValueError):
909
+ return None
910
+
911
+
912
+ def _direct_parameter_cast(parameter: exp.Parameter) -> "exp.Cast | None":
913
+ current: exp.Expression = parameter
914
+ parent = current.parent
915
+ while isinstance(parent, exp.Paren):
916
+ current = parent
917
+ parent = current.parent
918
+ if isinstance(parent, exp.Cast) and parent.this is current:
919
+ return parent
920
+ return None
921
+
922
+
923
+ @lru_cache(maxsize=256)
924
+ def _rewrite_postgres_uuid_placeholders(
925
+ sql: str, uuid_ordinals: "tuple[int, ...]", dialect: str
926
+ ) -> "tuple[str, tuple[int, ...]]":
927
+ """Wrap requested UUID parameter placeholders in explicit UUID casts.
928
+
929
+ Placeholders that already carry a different explicit cast stay authoritative
930
+ and are excluded from the returned effective ordinals. Results are memoized
931
+ per ``(sql, uuid_ordinals, dialect)`` so repeated executions of the same
932
+ statement shape reuse the rewrite without parsing again.
933
+ """
934
+ try:
935
+ expressions = cast(
936
+ "list[exp.Expression]",
937
+ [expression for expression in sqlglot.parse(sql, read=dialect) if expression is not None],
938
+ )
939
+ except ParseError as exc:
940
+ msg = f"Failed to parse PostgreSQL ADBC SQL for UUID parameter binding: {exc}"
941
+ raise SQLSpecError(msg) from exc
942
+ if not expressions:
943
+ msg = "Failed to parse PostgreSQL ADBC SQL for UUID parameter binding: SQLGlot returned no statements."
944
+ raise SQLSpecError(msg)
945
+
946
+ requested = set(uuid_ordinals)
947
+ authoritative: set[int] = set()
948
+ for expression in expressions:
949
+ for parameter in expression.find_all(exp.Parameter):
950
+ ordinal = _parameter_ordinal(parameter)
951
+ if ordinal is None or ordinal not in requested:
952
+ continue
953
+ cast_expression = _direct_parameter_cast(parameter)
954
+ if cast_expression is not None:
955
+ target = cast_expression.args.get("to")
956
+ target_kind = target.args.get("kind") if isinstance(target, exp.DataType) else None
957
+ if isinstance(target_kind, exp.Dot):
958
+ target_kind = target_kind.expression
959
+ is_uuid_cast = isinstance(target, exp.DataType) and (
960
+ target.this == exp.DataType.Type.UUID
961
+ or (
962
+ target.this == exp.DataType.Type.USERDEFINED
963
+ and isinstance(target_kind, exp.Identifier)
964
+ and target_kind.name.lower() == "uuid"
965
+ )
966
+ )
967
+ if not is_uuid_cast:
968
+ authoritative.add(ordinal)
969
+ effective = tuple(ordinal for ordinal in uuid_ordinals if ordinal not in authoritative)
970
+ effective_set = set(effective)
971
+
972
+ def wrap_parameter(node: exp.Expression) -> exp.Expression:
973
+ if not isinstance(node, exp.Parameter):
974
+ return node
975
+ ordinal = _parameter_ordinal(node)
976
+ if ordinal is None or ordinal not in effective_set:
977
+ return node
978
+ if _direct_parameter_cast(node) is not None:
979
+ return node
980
+ return exp.Cast(this=node.copy(), to=exp.DataType.build("UUID"))
981
+
982
+ for expression in expressions:
983
+ expression.transform(wrap_parameter, copy=False)
984
+ return "; ".join(expression.sql(dialect=dialect) for expression in expressions), effective
985
+
986
+
987
+ def _detect_batch_uuid_ordinals(parameters: Any) -> "tuple[int, ...]":
988
+ if not isinstance(parameters, (list, tuple)) or not parameters:
989
+ return ()
990
+ first_row = parameters[0]
991
+ if not isinstance(first_row, (list, tuple)):
992
+ return ()
993
+ expected_size = len(first_row)
994
+ for row in parameters:
995
+ if not isinstance(row, (list, tuple)) or len(row) != expected_size:
996
+ return ()
997
+ return tuple(
998
+ ordinal
999
+ for ordinal in range(1, expected_size + 1)
1000
+ if any(isinstance(row[ordinal - 1], _UUID_TYPES) for row in parameters)
1001
+ )
1002
+
1003
+
1004
+ def _convert_uuid_row(row: Any, ordinals: "tuple[int, ...]", row_number: int) -> Any:
1005
+ converted = list(row)
1006
+ for ordinal in ordinals:
1007
+ value = converted[ordinal - 1]
1008
+ if value is None:
1009
+ continue
1010
+ if not isinstance(value, _UUID_TYPES) and not isinstance(value, str):
1011
+ msg = (
1012
+ f"ADBC PostgreSQL UUID parameter ordinal {ordinal} has incompatible "
1013
+ f"{type(value).__name__} value in batch row {row_number}; expected a UUID object, "
1014
+ "parseable UUID string, or None."
1015
+ )
1016
+ raise SQLSpecError(msg)
1017
+ try:
1018
+ converted[ordinal - 1] = str(UUID(str(value)))
1019
+ except (AttributeError, TypeError, ValueError) as exc:
1020
+ msg = (
1021
+ f"ADBC PostgreSQL UUID parameter ordinal {ordinal} has incompatible "
1022
+ f"{type(value).__name__} value in batch row {row_number}; expected a UUID object, "
1023
+ "parseable UUID string, or None."
1024
+ )
1025
+ raise SQLSpecError(msg) from exc
1026
+ return tuple(converted) if isinstance(row, tuple) else converted
1027
+
1028
+
853
1029
  def _create_adbc_error(error: Any, error_class: type[SQLSpecError], description: str) -> SQLSpecError:
854
1030
  """Create an ADBC error instance without raising it."""
855
1031
  msg = f"ADBC {description}: {error}"
@@ -1005,3 +1181,6 @@ def _lift_flightsql_db_kwargs(config: "dict[str, Any]") -> None:
1005
1181
  config.pop("gizmosql_backend", None)
1006
1182
  if db_kwargs_dict:
1007
1183
  config["db_kwargs"] = db_kwargs_dict
1184
+
1185
+
1186
+ driver_profile: Final[DriverParameterProfile] = build_profile()
@@ -22,6 +22,7 @@ from sqlspec.adapters.adbc.core import (
22
22
  normalize_postgres_empty_parameters,
23
23
  normalize_script_rowcount,
24
24
  prepare_postgres_parameters,
25
+ prepare_postgres_uuid_bindings,
25
26
  resolve_column_names,
26
27
  resolve_dialect_name,
27
28
  resolve_many_rowcount,
@@ -141,14 +142,6 @@ class AdbcSelectStreamSource:
141
142
  cursor_manager.__exit__(None, None, None)
142
143
 
143
144
 
144
- def _fetch_record_batch(cursor: Any) -> Any:
145
- fetch_record_batch = getattr(cursor, "fetch_record_batch", None)
146
- if fetch_record_batch is None:
147
- msg = "ADBC cursor does not expose fetch_record_batch() for native row streaming."
148
- raise SQLSpecError(msg)
149
- return fetch_record_batch()
150
-
151
-
152
145
  @final
153
146
  class AdbcDriver(SyncDriverAdapterBase):
154
147
  """ADBC driver for Arrow Database Connectivity.
@@ -197,6 +190,16 @@ class AdbcDriver(SyncDriverAdapterBase):
197
190
  # CORE DISPATCH METHODS
198
191
  # ─────────────────────────────────────────────────────────────────────────────
199
192
 
193
+ def _compiled_sql(
194
+ self, statement: "SQL", statement_config: "StatementConfig", flatten_single_parameters: bool = False
195
+ ) -> "tuple[str, object]":
196
+ compiled_sql, prepared_parameters = super()._compiled_sql(
197
+ statement, statement_config, flatten_single_parameters=flatten_single_parameters
198
+ )
199
+ return prepare_postgres_uuid_bindings(
200
+ compiled_sql, prepared_parameters, is_many=statement.is_many, dialect=self._dialect_name
201
+ )
202
+
200
203
  def dispatch_execute(self, cursor: "AdbcRawCursor", statement: SQL) -> "ExecutionResult":
201
204
  """Execute single SQL statement.
202
205
 
@@ -755,4 +758,12 @@ class AdbcDriver(SyncDriverAdapterBase):
755
758
  return fallback
756
759
 
757
760
 
761
+ def _fetch_record_batch(cursor: Any) -> Any:
762
+ fetch_record_batch = getattr(cursor, "fetch_record_batch", None)
763
+ if fetch_record_batch is None:
764
+ msg = "ADBC cursor does not expose fetch_record_batch() for native row streaming."
765
+ raise SQLSpecError(msg)
766
+ return fetch_record_batch()
767
+
768
+
758
769
  register_driver_profile("adbc", driver_profile)
@@ -59,6 +59,78 @@ class ADBCStore(BaseSQLSpecStore["AdbcConfig"]):
59
59
  super().__init__(config)
60
60
  self._dialect: str | None = None
61
61
 
62
+ async def create_table(self) -> None:
63
+ """Create the session table if it doesn't exist."""
64
+ if not self.create_schema_enabled:
65
+ await self.reconcile_schema()
66
+ return
67
+ await async_(self._create_table)()
68
+ await self.reconcile_schema(assume_existing=True)
69
+
70
+ async def get(self, key: str, renew_for: "int | timedelta | None" = None) -> "bytes | None":
71
+ """Get a session value by key.
72
+
73
+ Args:
74
+ key: Session ID to retrieve.
75
+ renew_for: If given, renew the expiry time for this duration.
76
+
77
+ Returns:
78
+ Session data as bytes if found and not expired, None otherwise.
79
+ """
80
+ return await async_(self._get)(key, renew_for)
81
+
82
+ async def set(self, key: str, value: "str | bytes", expires_in: "int | timedelta | None" = None) -> None:
83
+ """Store a session value.
84
+
85
+ Args:
86
+ key: Session ID.
87
+ value: Session data.
88
+ expires_in: Time until expiration.
89
+ """
90
+ await async_(self._set)(key, value, expires_in)
91
+
92
+ async def delete(self, key: str) -> None:
93
+ """Delete a session by key.
94
+
95
+ Args:
96
+ key: Session ID to delete.
97
+ """
98
+ await async_(self._delete)(key)
99
+
100
+ async def delete_all(self) -> None:
101
+ """Delete all sessions from the store."""
102
+ await async_(self._delete_all)()
103
+
104
+ async def exists(self, key: str) -> bool:
105
+ """Check if a session key exists and is not expired.
106
+
107
+ Args:
108
+ key: Session ID to check.
109
+
110
+ Returns:
111
+ True if the session exists and is not expired.
112
+ """
113
+ return await async_(self._exists)(key)
114
+
115
+ async def expires_in(self, key: str) -> "int | None":
116
+ """Get the time in seconds until the session expires.
117
+
118
+ Args:
119
+ key: Session ID to check.
120
+
121
+ Returns:
122
+ Seconds until expiration, or None if no expiry or key doesn't exist.
123
+ """
124
+ return await async_(self._expires_in)(key)
125
+
126
+ async def delete_expired(self) -> int:
127
+ """Delete all expired sessions.
128
+
129
+ Returns:
130
+ Number of sessions deleted.
131
+ """
132
+ return await async_(self._delete_expired)()
133
+
62
134
  def _get_dialect(self) -> str:
63
135
  """Get the database dialect, caching it after first access.
64
136
 
@@ -220,10 +292,6 @@ class ADBCStore(BaseSQLSpecStore["AdbcConfig"]):
220
292
 
221
293
  return [f"DROP INDEX IF EXISTS idx_{self._table_name}_expires_at", f"DROP TABLE IF EXISTS {self._table_name}"]
222
294
 
223
- async def create_table(self) -> None:
224
- """Create the session table if it doesn't exist."""
225
- await async_(self._create_table)()
226
-
227
295
  def _get(self, key: str, renew_for: "int | timedelta | None" = None) -> "bytes | None":
228
296
  """Synchronous implementation of get using ADBC driver."""
229
297
  p1 = self._get_param_placeholder(1)
@@ -258,18 +326,6 @@ class ADBCStore(BaseSQLSpecStore["AdbcConfig"]):
258
326
 
259
327
  return bytes(data)
260
328
 
261
- async def get(self, key: str, renew_for: "int | timedelta | None" = None) -> "bytes | None":
262
- """Get a session value by key.
263
-
264
- Args:
265
- key: Session ID to retrieve.
266
- renew_for: If given, renew the expiry time for this duration.
267
-
268
- Returns:
269
- Session data as bytes if found and not expired, None otherwise.
270
- """
271
- return await async_(self._get)(key, renew_for)
272
-
273
329
  def _set(self, key: str, value: "str | bytes", expires_in: "int | timedelta | None" = None) -> None:
274
330
  """Synchronous implementation of set using ADBC driver with dialect-specific UPSERT."""
275
331
  data = self._value_to_bytes(value)
@@ -364,16 +420,6 @@ class ADBCStore(BaseSQLSpecStore["AdbcConfig"]):
364
420
  driver.execute(sql, key, data, expires_at)
365
421
  driver.commit()
366
422
 
367
- async def set(self, key: str, value: "str | bytes", expires_in: "int | timedelta | None" = None) -> None:
368
- """Store a session value.
369
-
370
- Args:
371
- key: Session ID.
372
- value: Session data.
373
- expires_in: Time until expiration.
374
- """
375
- await async_(self._set)(key, value, expires_in)
376
-
377
423
  def _delete(self, key: str) -> None:
378
424
  """Synchronous implementation of delete using ADBC driver."""
379
425
  p1 = self._get_param_placeholder(1)
@@ -383,14 +429,6 @@ class ADBCStore(BaseSQLSpecStore["AdbcConfig"]):
383
429
  driver.execute(sql, key)
384
430
  driver.commit()
385
431
 
386
- async def delete(self, key: str) -> None:
387
- """Delete a session by key.
388
-
389
- Args:
390
- key: Session ID to delete.
391
- """
392
- await async_(self._delete)(key)
393
-
394
432
  def _delete_all(self) -> None:
395
433
  """Synchronous implementation of delete_all using ADBC driver."""
396
434
 
@@ -400,10 +438,6 @@ class ADBCStore(BaseSQLSpecStore["AdbcConfig"]):
400
438
  driver.execute(sql)
401
439
  driver.commit()
402
440
 
403
- async def delete_all(self) -> None:
404
- """Delete all sessions from the store."""
405
- await async_(self._delete_all)()
406
-
407
441
  def _exists(self, key: str) -> bool:
408
442
  """Synchronous implementation of exists using ADBC driver."""
409
443
 
@@ -419,17 +453,6 @@ class ADBCStore(BaseSQLSpecStore["AdbcConfig"]):
419
453
  with self._config.provide_session() as driver:
420
454
  return bool(driver.select_one_or_none(sql, key) is not None)
421
455
 
422
- async def exists(self, key: str) -> bool:
423
- """Check if a session key exists and is not expired.
424
-
425
- Args:
426
- key: Session ID to check.
427
-
428
- Returns:
429
- True if the session exists and is not expired.
430
- """
431
- return await async_(self._exists)(key)
432
-
433
456
  def _expires_in(self, key: str) -> "int | None":
434
457
  """Synchronous implementation of expires_in using ADBC driver."""
435
458
  p1 = self._get_param_placeholder(1)
@@ -460,17 +483,6 @@ class ADBCStore(BaseSQLSpecStore["AdbcConfig"]):
460
483
  delta = expires_at - now
461
484
  return int(delta.total_seconds())
462
485
 
463
- async def expires_in(self, key: str) -> "int | None":
464
- """Get the time in seconds until the session expires.
465
-
466
- Args:
467
- key: Session ID to check.
468
-
469
- Returns:
470
- Seconds until expiration, or None if no expiry or key doesn't exist.
471
- """
472
- return await async_(self._expires_in)(key)
473
-
474
486
  def _delete_expired(self) -> int:
475
487
  """Synchronous implementation of delete_expired using ADBC driver."""
476
488
  current_ts = self._get_current_timestamp_expr()
@@ -500,11 +512,3 @@ class ADBCStore(BaseSQLSpecStore["AdbcConfig"]):
500
512
  if count > 0:
501
513
  self._log_delete_expired(count)
502
514
  return count
503
-
504
- async def delete_expired(self) -> int:
505
- """Delete all expired sessions.
506
-
507
- Returns:
508
- Number of sessions deleted.
509
- """
510
- return await async_(self._delete_expired)()
@@ -64,13 +64,16 @@ class AiomysqlADKStore(BaseAsyncADKStore["AiomysqlConfig"]):
64
64
 
65
65
  async def create_tables(self) -> None:
66
66
  """Create all ADK session tables if they don't exist."""
67
+ if not self.create_schema_enabled:
68
+ await self.reconcile_schema()
69
+ return
70
+
67
71
  async with self._config.provide_session() as driver:
68
72
  await driver.execute_script(await self._sessions_table_ddl())
69
73
  await driver.execute_script(await self._events_table_ddl())
70
74
  await driver.execute_script(await self._app_states_table_ddl())
71
75
  await driver.execute_script(await self._user_states_table_ddl())
72
76
  await driver.execute_script(await self._metadata_table_ddl())
73
- await driver.execute_script(await self._metadata_seed_sql())
74
77
 
75
78
  async def create_session(
76
79
  self, session_id: str, app_name: str, user_id: str, state: "dict[str, Any]", owner_id: "Any | None" = None
@@ -465,10 +468,6 @@ class AiomysqlADKStore(BaseAsyncADKStore["AiomysqlConfig"]):
465
468
  """Get MySQL CREATE TABLE SQL for ADK metadata."""
466
469
  return _mysql_metadata_ddl(self._metadata_table)
467
470
 
468
- async def _metadata_seed_sql(self) -> str:
469
- """Get MySQL metadata seed SQL."""
470
- return f"INSERT IGNORE INTO {self._metadata_table} (`key`, value) VALUES ('schema_version', '1')"
471
-
472
471
  def _drop_app_states_table_sql(self) -> str:
473
472
  """Get MySQL DROP TABLE SQL for app-scoped state."""
474
473
  return f"DROP TABLE IF EXISTS {self._app_state_table}"
@@ -503,6 +502,10 @@ class AiomysqlADKMemoryStore(BaseAsyncADKMemoryStore["AiomysqlConfig"]):
503
502
 
504
503
  async def create_tables(self) -> None:
505
504
  """Create the memory table and indexes if they don't exist."""
505
+ if not self.create_schema_enabled:
506
+ await self.reconcile_schema()
507
+ return
508
+
506
509
  if not self._enabled:
507
510
  return
508
511