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.
- sqlspec/adapters/adbc/adk/store.py +8 -16
- sqlspec/adapters/adbc/core.py +182 -3
- sqlspec/adapters/adbc/driver.py +19 -8
- sqlspec/adapters/adbc/litestar/store.py +72 -68
- sqlspec/adapters/aiomysql/adk/store.py +8 -5
- sqlspec/adapters/aiomysql/core.py +107 -106
- sqlspec/adapters/aiomysql/events/store.py +19 -23
- sqlspec/adapters/aiomysql/litestar/store.py +70 -30
- sqlspec/adapters/aiosqlite/adk/store.py +62 -63
- sqlspec/adapters/aiosqlite/config.py +146 -110
- sqlspec/adapters/aiosqlite/core.py +216 -217
- sqlspec/adapters/aiosqlite/driver.py +172 -172
- sqlspec/adapters/aiosqlite/events/store.py +16 -2
- sqlspec/adapters/aiosqlite/litestar/store.py +73 -58
- sqlspec/adapters/arrow_odbc/adk/store.py +131 -134
- sqlspec/adapters/arrow_odbc/core.py +13 -12
- sqlspec/adapters/arrow_odbc/driver.py +13 -13
- sqlspec/adapters/arrow_odbc/litestar/store.py +4 -0
- sqlspec/adapters/asyncmy/adk/store.py +8 -5
- sqlspec/adapters/asyncmy/core.py +107 -106
- sqlspec/adapters/asyncmy/events/store.py +19 -23
- sqlspec/adapters/asyncmy/litestar/store.py +70 -30
- sqlspec/adapters/asyncpg/adk/store.py +90 -50
- sqlspec/adapters/asyncpg/core.py +106 -107
- sqlspec/adapters/asyncpg/driver.py +88 -88
- sqlspec/adapters/asyncpg/events/store.py +32 -0
- sqlspec/adapters/asyncpg/litestar/store.py +59 -33
- sqlspec/adapters/bigquery/adk/store.py +4 -9
- sqlspec/adapters/bigquery/core.py +374 -375
- sqlspec/adapters/bigquery/driver.py +108 -108
- sqlspec/adapters/bigquery/events/store.py +58 -16
- sqlspec/adapters/bigquery/litestar/store.py +113 -69
- sqlspec/adapters/cockroach_asyncpg/adk/store.py +8 -8
- sqlspec/adapters/cockroach_asyncpg/driver.py +19 -19
- sqlspec/adapters/cockroach_asyncpg/litestar/store.py +47 -19
- sqlspec/adapters/cockroach_psycopg/adk/store.py +16 -15
- sqlspec/adapters/cockroach_psycopg/driver.py +36 -36
- sqlspec/adapters/cockroach_psycopg/litestar/store.py +83 -44
- sqlspec/adapters/duckdb/adk/store.py +8 -9
- sqlspec/adapters/duckdb/core.py +7 -8
- sqlspec/adapters/duckdb/driver.py +4 -4
- sqlspec/adapters/duckdb/litestar/store.py +72 -68
- sqlspec/adapters/mssql_python/adk/store.py +42 -35
- sqlspec/adapters/mssql_python/core.py +25 -6
- sqlspec/adapters/mssql_python/driver.py +31 -13
- sqlspec/adapters/mssql_python/litestar/store.py +36 -32
- sqlspec/adapters/mysqlconnector/adk/store.py +16 -10
- sqlspec/adapters/mysqlconnector/core.py +86 -87
- sqlspec/adapters/mysqlconnector/events/store.py +38 -35
- sqlspec/adapters/mysqlconnector/litestar/store.py +93 -45
- sqlspec/adapters/oracledb/_json_handlers.py +65 -60
- sqlspec/adapters/oracledb/_storage.py +215 -0
- sqlspec/adapters/oracledb/adk/store.py +183 -358
- sqlspec/adapters/oracledb/config.py +142 -124
- sqlspec/adapters/oracledb/core.py +212 -213
- sqlspec/adapters/oracledb/data_dictionary.py +495 -309
- sqlspec/adapters/oracledb/driver.py +49 -60
- sqlspec/adapters/oracledb/events/store.py +239 -238
- sqlspec/adapters/oracledb/litestar/store.py +223 -172
- sqlspec/adapters/psqlpy/adk/store.py +52 -12
- sqlspec/adapters/psqlpy/core.py +244 -211
- sqlspec/adapters/psqlpy/driver.py +31 -15
- sqlspec/adapters/psqlpy/events/store.py +32 -0
- sqlspec/adapters/psqlpy/litestar/store.py +59 -33
- sqlspec/adapters/psycopg/adk/store.py +72 -23
- sqlspec/adapters/psycopg/core.py +54 -55
- sqlspec/adapters/psycopg/driver.py +294 -250
- sqlspec/adapters/psycopg/events/store.py +41 -0
- sqlspec/adapters/psycopg/litestar/store.py +138 -102
- sqlspec/adapters/pymssql/adk/store.py +65 -57
- sqlspec/adapters/pymssql/core.py +36 -19
- sqlspec/adapters/pymssql/driver.py +4 -4
- sqlspec/adapters/pymssql/litestar/store.py +36 -32
- sqlspec/adapters/pymysql/adk/store.py +8 -5
- sqlspec/adapters/pymysql/core.py +86 -87
- sqlspec/adapters/pymysql/events/store.py +15 -11
- sqlspec/adapters/pymysql/litestar/store.py +67 -27
- sqlspec/adapters/spanner/adk/store.py +8 -8
- sqlspec/adapters/spanner/core.py +28 -29
- sqlspec/adapters/spanner/driver.py +224 -224
- sqlspec/adapters/spanner/litestar/store.py +73 -64
- sqlspec/adapters/sqlite/adk/store.py +62 -63
- sqlspec/adapters/sqlite/config.py +129 -93
- sqlspec/adapters/sqlite/core.py +170 -171
- sqlspec/adapters/sqlite/driver.py +178 -178
- sqlspec/adapters/sqlite/events/store.py +16 -2
- sqlspec/adapters/sqlite/litestar/store.py +85 -70
- sqlspec/builder/_base.py +60 -58
- sqlspec/builder/_dml.py +1 -1
- sqlspec/config.py +335 -234
- sqlspec/core/hashing.py +34 -17
- sqlspec/data_dictionary/dialects/oracle.py +35 -0
- sqlspec/data_dictionary/sql/oracle/version.sql +6 -0
- sqlspec/data_dictionary/sql/spanner/change_streams/googlesql/list.sql +2 -2
- sqlspec/data_dictionary/sql/spanner/change_streams/googlesql/tables.sql +2 -2
- sqlspec/data_dictionary/sql/spanner/columns/googlesql/by_schema.sql +2 -2
- sqlspec/data_dictionary/sql/spanner/columns.sql +2 -2
- sqlspec/data_dictionary/sql/spanner/constraints/googlesql/by_schema.sql +2 -2
- sqlspec/data_dictionary/sql/spanner/foreign_keys.sql +5 -5
- sqlspec/data_dictionary/sql/spanner/indexes/googlesql/by_schema.sql +2 -2
- sqlspec/data_dictionary/sql/spanner/indexes.sql +2 -2
- sqlspec/data_dictionary/sql/spanner/privileges/googlesql/by_schema.sql +2 -2
- sqlspec/data_dictionary/sql/spanner/property_graphs/googlesql/list.sql +2 -2
- sqlspec/data_dictionary/sql/spanner/routines/googlesql/by_schema.sql +2 -2
- sqlspec/data_dictionary/sql/spanner/routines/googlesql/parameters_by_schema.sql +2 -2
- sqlspec/data_dictionary/sql/spanner/sequences/googlesql/by_schema.sql +2 -2
- sqlspec/data_dictionary/sql/spanner/system/googlesql/query_stats_top.sql +1 -1
- sqlspec/data_dictionary/sql/spanner/system/googlesql/table_sizes.sql +2 -2
- sqlspec/data_dictionary/sql/spanner/tables/googlesql/by_schema.sql +2 -2
- sqlspec/data_dictionary/sql/spanner/tables.sql +1 -1
- sqlspec/data_dictionary/sql/spanner/views/googlesql/by_schema.sql +2 -2
- sqlspec/extensions/adk/_config_utils.py +27 -2
- sqlspec/extensions/adk/memory/store.py +64 -3
- sqlspec/extensions/adk/migrations/0002_reset_adk_tables.py +15 -1
- sqlspec/extensions/adk/store.py +86 -16
- sqlspec/extensions/events/_store.py +70 -1
- sqlspec/extensions/events/migrations/0001_create_event_queue.py +43 -1
- sqlspec/extensions/litestar/config.py +44 -0
- sqlspec/extensions/litestar/migrations/0001_create_session_table.py +5 -0
- sqlspec/extensions/litestar/store.py +108 -25
- sqlspec/migrations/__init__.py +5 -0
- sqlspec/migrations/base.py +93 -125
- sqlspec/migrations/schema.py +402 -0
- sqlspec/migrations/tracker.py +51 -118
- {sqlspec-0.55.0.dist-info → sqlspec-0.56.0.dist-info}/METADATA +18 -21
- {sqlspec-0.55.0.dist-info → sqlspec-0.56.0.dist-info}/RECORD +129 -127
- {sqlspec-0.55.0.dist-info → sqlspec-0.56.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.55.0.dist-info → sqlspec-0.56.0.dist-info}/entry_points.txt +0 -0
- {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:
|
sqlspec/adapters/adbc/core.py
CHANGED
|
@@ -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()
|
sqlspec/adapters/adbc/driver.py
CHANGED
|
@@ -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
|
|