sqlspec 0.70.0__py3-none-any.whl → 0.71.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/oracledb/driver.py +8 -3
- sqlspec/adapters/oracledb/events/backend.py +3 -2
- sqlspec/adapters/psqlpy/events/backend.py +3 -2
- sqlspec/adapters/psycopg/events/backend.py +5 -4
- sqlspec/adapters/sybase/adk/store.py +25 -22
- sqlspec/adapters/sybase/config.py +8 -6
- sqlspec/adapters/sybase/core.py +19 -7
- sqlspec/adapters/sybase/data_dictionary.py +88 -11
- sqlspec/adapters/sybase/events/store.py +5 -10
- sqlspec/adapters/sybase/litestar/store.py +2 -2
- sqlspec/adapters/sybase/migrations.py +1 -1
- sqlspec/adapters/sybase/pool.py +17 -11
- sqlspec/cli.py +6 -12
- sqlspec/config.py +5 -0
- sqlspec/core/result/_base.py +8 -2
- sqlspec/data_dictionary/dialects/sybase/sql/columns.sql +26 -12
- sqlspec/data_dictionary/dialects/sybase/sql/constraints.sql +39 -49
- sqlspec/data_dictionary/dialects/sybase/sql/foreign_keys.sql +6 -2
- sqlspec/data_dictionary/dialects/sybase/sql/views.sql +7 -3
- sqlspec/dialects/sybase/__init__.py +1 -1
- sqlspec/driver/_async.py +1 -0
- sqlspec/driver/_storage_helpers.py +81 -19
- sqlspec/driver/_sync.py +1 -0
- sqlspec/extensions/adk/_config_utils.py +44 -13
- sqlspec/extensions/adk/memory/store.py +1 -0
- sqlspec/extensions/adk/migrations/0002_create_mssql_python_memory.py +2 -1
- sqlspec/extensions/adk/store.py +1 -0
- sqlspec/extensions/events/_channel.py +32 -21
- sqlspec/extensions/events/_hints.py +2 -11
- sqlspec/extensions/events/_store.py +4 -1
- sqlspec/extensions/events/migrations/0001_create_event_queue.py +14 -9
- sqlspec/extensions/litestar/migrations/0001_create_session_table.py +18 -11
- sqlspec/extensions/litestar/store.py +4 -1
- sqlspec/observability/_common.py +2 -0
- sqlspec/storage/_arrow_payload.py +3 -1
- sqlspec/utils/adapters.py +186 -0
- sqlspec/utils/arrow_helpers.py +34 -26
- {sqlspec-0.70.0.dist-info → sqlspec-0.71.0.dist-info}/METADATA +1 -1
- {sqlspec-0.70.0.dist-info → sqlspec-0.71.0.dist-info}/RECORD +42 -41
- {sqlspec-0.70.0.dist-info → sqlspec-0.71.0.dist-info}/WHEEL +1 -1
- {sqlspec-0.70.0.dist-info → sqlspec-0.71.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.70.0.dist-info → sqlspec-0.71.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1251,10 +1251,15 @@ class OracleSyncDriver(OraclePipelineMixin, SyncDriverAdapterBase):
|
|
|
1251
1251
|
batch_table = coerce_oracle_df_to_arrow_table(oracle_df, import_pyarrow())
|
|
1252
1252
|
column_names = normalize_column_names(batch_table.column_names, self.driver_features)
|
|
1253
1253
|
if column_names != batch_table.column_names:
|
|
1254
|
-
batch_table = batch_table.rename_columns(column_names)
|
|
1254
|
+
batch_table = batch_table.rename_columns(column_names).replace_schema_metadata(
|
|
1255
|
+
batch_table.schema.metadata
|
|
1256
|
+
)
|
|
1255
1257
|
if arrow_schema is not None:
|
|
1256
1258
|
batch_table = batch_table.cast(arrow_schema)
|
|
1257
|
-
|
|
1259
|
+
if batch_table.num_rows:
|
|
1260
|
+
yield from batch_table.to_batches()
|
|
1261
|
+
else:
|
|
1262
|
+
yield import_pyarrow().RecordBatch.from_pylist([], schema=batch_table.schema)
|
|
1258
1263
|
except OracleError as exc:
|
|
1259
1264
|
raise create_mapped_exception(exc) from exc
|
|
1260
1265
|
|
|
@@ -1760,7 +1765,7 @@ class OracleAsyncDriver(OraclePipelineMixin, AsyncDriverAdapterBase):
|
|
|
1760
1765
|
rows_affected = sum(batch.num_rows for batch in record_batches)
|
|
1761
1766
|
pa = import_pyarrow()
|
|
1762
1767
|
if batch_schema is None:
|
|
1763
|
-
batch_schema = pa.schema([])
|
|
1768
|
+
batch_schema = arrow_schema if arrow_schema is not None else pa.schema([])
|
|
1764
1769
|
arrow_data: object
|
|
1765
1770
|
if return_format == "reader":
|
|
1766
1771
|
arrow_data = pa.RecordBatchReader.from_batches(batch_schema, record_batches)
|
|
@@ -9,6 +9,7 @@ from sqlspec.adapters.oracledb._typing import DEQ_IMMEDIATE, DEQ_ON_COMMIT
|
|
|
9
9
|
from sqlspec.adapters.oracledb.events._hub import OracleAsyncAQHub, OracleSyncAQHub
|
|
10
10
|
from sqlspec.exceptions import ImproperConfigurationError, MissingDependencyError
|
|
11
11
|
from sqlspec.extensions.events import EventMessage, parse_event_timestamp
|
|
12
|
+
from sqlspec.utils.adapters import resolve_adapter_name
|
|
12
13
|
from sqlspec.utils.logging import get_logger, log_with_context
|
|
13
14
|
from sqlspec.utils.serializers import from_json
|
|
14
15
|
from sqlspec.utils.uuids import uuid4
|
|
@@ -42,7 +43,7 @@ class OracleSyncAQEventBackend:
|
|
|
42
43
|
backend_name = "aq"
|
|
43
44
|
|
|
44
45
|
def __init__(self, config: "OracleSyncConfig", settings: "dict[str, Any] | None" = None) -> None:
|
|
45
|
-
if "oracledb"
|
|
46
|
+
if resolve_adapter_name(config) != "oracledb":
|
|
46
47
|
msg = "Oracle AQ backend requires an Oracle adapter"
|
|
47
48
|
raise ImproperConfigurationError(msg)
|
|
48
49
|
if config.is_async:
|
|
@@ -123,7 +124,7 @@ class OracleAsyncAQEventBackend:
|
|
|
123
124
|
backend_name = "aq"
|
|
124
125
|
|
|
125
126
|
def __init__(self, config: "OracleAsyncConfig", settings: "dict[str, Any] | None" = None) -> None:
|
|
126
|
-
if "oracledb"
|
|
127
|
+
if resolve_adapter_name(config) != "oracledb":
|
|
127
128
|
msg = "Oracle AQ backend requires an Oracle adapter"
|
|
128
129
|
raise ImproperConfigurationError(msg)
|
|
129
130
|
if not config.is_async:
|
|
@@ -16,6 +16,7 @@ from sqlspec.extensions.events import (
|
|
|
16
16
|
decode_notify_payload,
|
|
17
17
|
encode_notify_payload,
|
|
18
18
|
)
|
|
19
|
+
from sqlspec.utils.adapters import resolve_adapter_name
|
|
19
20
|
from sqlspec.utils.logging import get_logger, log_with_context
|
|
20
21
|
from sqlspec.utils.serializers import from_json, to_json
|
|
21
22
|
from sqlspec.utils.uuids import uuid4
|
|
@@ -88,7 +89,7 @@ class PsqlpyEventsBackend:
|
|
|
88
89
|
backend_name = "notify"
|
|
89
90
|
|
|
90
91
|
def __init__(self, config: "PsqlpyConfig") -> None:
|
|
91
|
-
if "psqlpy"
|
|
92
|
+
if resolve_adapter_name(config) != "psqlpy":
|
|
92
93
|
msg = "Psqlpy events backend requires a Psqlpy adapter"
|
|
93
94
|
raise ImproperConfigurationError(msg)
|
|
94
95
|
self._config = config
|
|
@@ -177,7 +178,7 @@ class PsqlpyHybridEventsBackend:
|
|
|
177
178
|
backend_name = "notify_queue"
|
|
178
179
|
|
|
179
180
|
def __init__(self, config: "PsqlpyConfig", queue: "AsyncTableEventQueue") -> None:
|
|
180
|
-
if "psqlpy"
|
|
181
|
+
if resolve_adapter_name(config) != "psqlpy":
|
|
181
182
|
msg = "Psqlpy hybrid backend requires a Psqlpy adapter"
|
|
182
183
|
raise ImproperConfigurationError(msg)
|
|
183
184
|
self._config = config
|
|
@@ -17,6 +17,7 @@ from sqlspec.extensions.events import (
|
|
|
17
17
|
decode_notify_payload,
|
|
18
18
|
encode_notify_payload,
|
|
19
19
|
)
|
|
20
|
+
from sqlspec.utils.adapters import resolve_adapter_name
|
|
20
21
|
from sqlspec.utils.logging import get_logger, log_with_context
|
|
21
22
|
from sqlspec.utils.serializers import from_json, to_json
|
|
22
23
|
from sqlspec.utils.uuids import uuid4
|
|
@@ -103,7 +104,7 @@ class PsycopgSyncEventsBackend:
|
|
|
103
104
|
backend_name = "notify"
|
|
104
105
|
|
|
105
106
|
def __init__(self, config: "PsycopgSyncConfig") -> None:
|
|
106
|
-
if
|
|
107
|
+
if resolve_adapter_name(config) not in {"psycopg", "cockroach_psycopg"}:
|
|
107
108
|
msg = "Psycopg events backend requires a Psycopg adapter"
|
|
108
109
|
raise ImproperConfigurationError(msg)
|
|
109
110
|
if config.is_async:
|
|
@@ -202,7 +203,7 @@ class PsycopgAsyncEventsBackend:
|
|
|
202
203
|
backend_name = "notify"
|
|
203
204
|
|
|
204
205
|
def __init__(self, config: "PsycopgAsyncConfig") -> None:
|
|
205
|
-
if
|
|
206
|
+
if resolve_adapter_name(config) not in {"psycopg", "cockroach_psycopg"}:
|
|
206
207
|
msg = "Psycopg events backend requires a Psycopg adapter"
|
|
207
208
|
raise ImproperConfigurationError(msg)
|
|
208
209
|
if not config.is_async:
|
|
@@ -292,7 +293,7 @@ class PsycopgSyncHybridEventsBackend:
|
|
|
292
293
|
backend_name = "notify_queue"
|
|
293
294
|
|
|
294
295
|
def __init__(self, config: "PsycopgSyncConfig", queue: "SyncTableEventQueue") -> None:
|
|
295
|
-
if
|
|
296
|
+
if resolve_adapter_name(config) not in {"psycopg", "cockroach_psycopg"}:
|
|
296
297
|
msg = "Psycopg hybrid backend requires a Psycopg adapter"
|
|
297
298
|
raise ImproperConfigurationError(msg)
|
|
298
299
|
if config.is_async:
|
|
@@ -473,7 +474,7 @@ class PsycopgAsyncHybridEventsBackend:
|
|
|
473
474
|
backend_name = "notify_queue"
|
|
474
475
|
|
|
475
476
|
def __init__(self, config: "PsycopgAsyncConfig", queue: "AsyncTableEventQueue") -> None:
|
|
476
|
-
if
|
|
477
|
+
if resolve_adapter_name(config) not in {"psycopg", "cockroach_psycopg"}:
|
|
477
478
|
msg = "Psycopg hybrid backend requires a Psycopg adapter"
|
|
478
479
|
raise ImproperConfigurationError(msg)
|
|
479
480
|
if not config.is_async:
|
|
@@ -370,15 +370,15 @@ class SybaseADKStore(BaseSyncADKStore["SybaseConfig"]):
|
|
|
370
370
|
|
|
371
371
|
def _drop_app_states_table_sql(self) -> str:
|
|
372
372
|
ref = escape_sql_literal(_table_ref(self._app_state_table))
|
|
373
|
-
return f"IF OBJECT_ID('{ref}'
|
|
373
|
+
return f"IF OBJECT_ID('{ref}') IS NOT NULL EXECUTE('DROP TABLE {ref}')"
|
|
374
374
|
|
|
375
375
|
def _drop_user_states_table_sql(self) -> str:
|
|
376
376
|
ref = escape_sql_literal(_table_ref(self._user_state_table))
|
|
377
|
-
return f"IF OBJECT_ID('{ref}'
|
|
377
|
+
return f"IF OBJECT_ID('{ref}') IS NOT NULL EXECUTE('DROP TABLE {ref}')"
|
|
378
378
|
|
|
379
379
|
def _drop_metadata_table_sql(self) -> str:
|
|
380
380
|
ref = escape_sql_literal(_table_ref(self._metadata_table))
|
|
381
|
-
return f"IF OBJECT_ID('{ref}'
|
|
381
|
+
return f"IF OBJECT_ID('{ref}') IS NOT NULL EXECUTE('DROP TABLE {ref}')"
|
|
382
382
|
|
|
383
383
|
def _drop_tables_sql(self) -> "list[str]":
|
|
384
384
|
events_ref = escape_sql_literal(_table_ref(self._events_table))
|
|
@@ -387,8 +387,8 @@ class SybaseADKStore(BaseSyncADKStore["SybaseConfig"]):
|
|
|
387
387
|
self._drop_metadata_table_sql(),
|
|
388
388
|
self._drop_user_states_table_sql(),
|
|
389
389
|
self._drop_app_states_table_sql(),
|
|
390
|
-
f"IF OBJECT_ID('{events_ref}'
|
|
391
|
-
f"IF OBJECT_ID('{sessions_ref}'
|
|
390
|
+
f"IF OBJECT_ID('{events_ref}') IS NOT NULL EXECUTE('DROP TABLE {events_ref}')",
|
|
391
|
+
f"IF OBJECT_ID('{sessions_ref}') IS NOT NULL EXECUTE('DROP TABLE {sessions_ref}')",
|
|
392
392
|
]
|
|
393
393
|
|
|
394
394
|
def _upsert_app_state_sql(self) -> str:
|
|
@@ -540,15 +540,14 @@ class SybaseADKMemoryStore(BaseSyncADKMemoryStore["SybaseConfig"]):
|
|
|
540
540
|
where_scope, scope_params = _build_sybase_scope_where(app_name, user_id, scope_filter)
|
|
541
541
|
escaped_query = _escape_like_pattern(query)
|
|
542
542
|
sql = f"""
|
|
543
|
-
SELECT
|
|
543
|
+
SELECT TOP %s
|
|
544
544
|
id, session_id, app_name, user_id, scope, event_id, author, timestamp,
|
|
545
545
|
content_json, content_text, metadata_json, inserted_at
|
|
546
546
|
FROM {_table_ref(self._memory_table)}
|
|
547
547
|
WHERE {where_scope} AND content_text LIKE %s ESCAPE '\\'
|
|
548
548
|
ORDER BY timestamp DESC
|
|
549
|
-
ROWS LIMIT %s
|
|
550
549
|
"""
|
|
551
|
-
rows = self._execute_fetchall(sql, (*scope_params, f"%{escaped_query}%"
|
|
550
|
+
rows = self._execute_fetchall(sql, (limit_value, *scope_params, f"%{escaped_query}%"))
|
|
552
551
|
return [_memory_record_from_row(row) for row in rows]
|
|
553
552
|
|
|
554
553
|
def delete_entries_by_session(self, session_id: str) -> int:
|
|
@@ -594,7 +593,7 @@ class SybaseADKMemoryStore(BaseSyncADKMemoryStore["SybaseConfig"]):
|
|
|
594
593
|
f"CONSTRAINT {_constraint_ref('uq', self._memory_table, 'event_id')} UNIQUE (event_id)"
|
|
595
594
|
")"
|
|
596
595
|
)
|
|
597
|
-
return f"IF OBJECT_ID('{escaped_ref}'
|
|
596
|
+
return f"IF OBJECT_ID('{escaped_ref}') IS NULL EXECUTE('{create_sql}');"
|
|
598
597
|
|
|
599
598
|
def _memory_index_specs(self) -> "list[tuple[str, str, str]]":
|
|
600
599
|
"""Return ``(index_name, table, columns)`` specs for memory-table indexes."""
|
|
@@ -608,7 +607,7 @@ class SybaseADKMemoryStore(BaseSyncADKMemoryStore["SybaseConfig"]):
|
|
|
608
607
|
|
|
609
608
|
def _drop_memory_table_sql(self) -> "list[str]":
|
|
610
609
|
ref = escape_sql_literal(_table_ref(self._memory_table))
|
|
611
|
-
return [f"IF OBJECT_ID('{ref}'
|
|
610
|
+
return [f"IF OBJECT_ID('{ref}') IS NOT NULL EXECUTE('DROP TABLE {ref}')"]
|
|
612
611
|
|
|
613
612
|
def _execute_fetchall(self, sql: str, params: "tuple[Any, ...]" = ()) -> "list[Any]":
|
|
614
613
|
with self._config.provide_connection() as conn, SybaseCursor(conn, as_dict=False) as cursor:
|
|
@@ -656,7 +655,7 @@ def _sessions_table_ddl(table: str, json_column_type: str, owner_id_column_ddl:
|
|
|
656
655
|
f"CONSTRAINT {_constraint_ref('pk', table, 'id')} PRIMARY KEY (id)"
|
|
657
656
|
")"
|
|
658
657
|
)
|
|
659
|
-
return f"IF OBJECT_ID('{escaped_ref}'
|
|
658
|
+
return f"IF OBJECT_ID('{escaped_ref}') IS NULL EXECUTE('{create_sql}');"
|
|
660
659
|
|
|
661
660
|
|
|
662
661
|
def _sessions_index_specs(table: str) -> "list[tuple[str, str, str]]":
|
|
@@ -684,7 +683,7 @@ def _events_table_ddl(table: str, session_table: str, json_column_type: str) ->
|
|
|
684
683
|
f"CONSTRAINT {_constraint_ref('fk', table, 'session')} FOREIGN KEY (session_id) REFERENCES {session_ref}(id)"
|
|
685
684
|
")"
|
|
686
685
|
)
|
|
687
|
-
return f"IF OBJECT_ID('{escaped_ref}'
|
|
686
|
+
return f"IF OBJECT_ID('{escaped_ref}') IS NULL EXECUTE('{create_sql}');"
|
|
688
687
|
|
|
689
688
|
|
|
690
689
|
def _events_index_specs(table: str) -> "list[tuple[str, str, str]]":
|
|
@@ -709,7 +708,7 @@ def _app_states_table_ddl(table: str, json_column_type: str) -> str:
|
|
|
709
708
|
f"CONSTRAINT {_constraint_ref('pk', table, 'app_name')} PRIMARY KEY (app_name)"
|
|
710
709
|
")"
|
|
711
710
|
)
|
|
712
|
-
return f"IF OBJECT_ID('{escaped_ref}'
|
|
711
|
+
return f"IF OBJECT_ID('{escaped_ref}') IS NULL EXECUTE('{create_sql}');"
|
|
713
712
|
|
|
714
713
|
|
|
715
714
|
def _user_states_table_ddl(table: str, json_column_type: str) -> str:
|
|
@@ -724,7 +723,7 @@ def _user_states_table_ddl(table: str, json_column_type: str) -> str:
|
|
|
724
723
|
f"CONSTRAINT {_constraint_ref('pk', table, 'app_user')} PRIMARY KEY (app_name, user_id)"
|
|
725
724
|
")"
|
|
726
725
|
)
|
|
727
|
-
return f"IF OBJECT_ID('{escaped_ref}'
|
|
726
|
+
return f"IF OBJECT_ID('{escaped_ref}') IS NULL EXECUTE('{create_sql}');"
|
|
728
727
|
|
|
729
728
|
|
|
730
729
|
def _metadata_table_ddl(table: str) -> str:
|
|
@@ -737,7 +736,7 @@ def _metadata_table_ddl(table: str) -> str:
|
|
|
737
736
|
f"CONSTRAINT {_constraint_ref('pk', table, 'key')} PRIMARY KEY ([key])"
|
|
738
737
|
")"
|
|
739
738
|
)
|
|
740
|
-
return f"IF OBJECT_ID('{escaped_ref}'
|
|
739
|
+
return f"IF OBJECT_ID('{escaped_ref}') IS NULL EXECUTE('{create_sql}');"
|
|
741
740
|
|
|
742
741
|
|
|
743
742
|
def _create_index_sql(table: str, index_name: str, columns: str) -> str:
|
|
@@ -794,15 +793,15 @@ def _events_query(
|
|
|
794
793
|
if after_timestamp is not None:
|
|
795
794
|
after_clause = " AND timestamp > %s"
|
|
796
795
|
params.append(after_timestamp)
|
|
797
|
-
|
|
796
|
+
top_clause = ""
|
|
798
797
|
if limit is not None:
|
|
799
|
-
|
|
800
|
-
params.
|
|
798
|
+
top_clause = "TOP %s "
|
|
799
|
+
params.insert(0, limit)
|
|
801
800
|
sql = f"""
|
|
802
|
-
SELECT id, app_name, user_id, session_id, invocation_id, timestamp, event_data
|
|
801
|
+
SELECT {top_clause}id, app_name, user_id, session_id, invocation_id, timestamp, event_data
|
|
803
802
|
FROM {_table_ref(table)}
|
|
804
803
|
WHERE app_name = %s AND user_id = %s AND session_id = %s{after_clause}
|
|
805
|
-
ORDER BY timestamp ASC
|
|
804
|
+
ORDER BY timestamp ASC
|
|
806
805
|
"""
|
|
807
806
|
return sql, tuple(params)
|
|
808
807
|
|
|
@@ -947,13 +946,17 @@ def _session_list_query(
|
|
|
947
946
|
params.append(user_id)
|
|
948
947
|
where_clause = f"{where_clause} AND user_id = %s"
|
|
949
948
|
|
|
949
|
+
top_clause = ""
|
|
950
950
|
page_clause = ""
|
|
951
|
-
if limit is not None:
|
|
951
|
+
if limit is not None and offset == 0:
|
|
952
|
+
top_clause = "TOP %s "
|
|
953
|
+
params.insert(0, limit)
|
|
954
|
+
elif limit is not None:
|
|
952
955
|
params.extend((limit, offset))
|
|
953
956
|
page_clause = "\n ROWS LIMIT %s OFFSET %s"
|
|
954
957
|
|
|
955
958
|
sql = f"""
|
|
956
|
-
SELECT id, app_name, user_id, state, create_time, update_time
|
|
959
|
+
SELECT {top_clause}id, app_name, user_id, state, create_time, update_time
|
|
957
960
|
FROM {_table_ref(session_table)}
|
|
958
961
|
WHERE {where_clause}
|
|
959
962
|
ORDER BY {column} {direction}, id {direction}{page_clause}
|
|
@@ -38,9 +38,11 @@ class SybaseConnectionParams(TypedDict):
|
|
|
38
38
|
host: NotRequired[str]
|
|
39
39
|
"""Alias for ``server``."""
|
|
40
40
|
freetds_server: NotRequired[str]
|
|
41
|
-
"""FreeTDS server configuration section name."""
|
|
41
|
+
"""FreeTDS server configuration section name; requires an explicit port."""
|
|
42
42
|
server_alias: NotRequired[str]
|
|
43
|
-
"""
|
|
43
|
+
"""FreeTDS server configuration section name; requires an explicit port."""
|
|
44
|
+
is_server_alias: NotRequired[bool]
|
|
45
|
+
"""Treat ``server`` as a FreeTDS configuration section name; requires an explicit port."""
|
|
44
46
|
user: NotRequired[str]
|
|
45
47
|
"""Login user name."""
|
|
46
48
|
username: NotRequired[str]
|
|
@@ -52,7 +54,7 @@ class SybaseConnectionParams(TypedDict):
|
|
|
52
54
|
db: NotRequired[str]
|
|
53
55
|
"""Alias for ``database``."""
|
|
54
56
|
port: NotRequired[int | str]
|
|
55
|
-
"""TCP port number
|
|
57
|
+
"""TCP port number; required for section aliases, otherwise defaults to 5000."""
|
|
56
58
|
timeout: NotRequired[SybaseTimeout]
|
|
57
59
|
"""Query execution timeout in seconds."""
|
|
58
60
|
login_timeout: NotRequired[SybaseTimeout]
|
|
@@ -64,7 +66,7 @@ class SybaseConnectionParams(TypedDict):
|
|
|
64
66
|
appname: NotRequired[str]
|
|
65
67
|
"""Application name sent to the server for connection identification."""
|
|
66
68
|
conn_properties: NotRequired[str]
|
|
67
|
-
"""
|
|
69
|
+
"""Session initialization SQL; replaces the default connection properties when supplied."""
|
|
68
70
|
autocommit: NotRequired[bool]
|
|
69
71
|
"""Enable autocommit mode for transactions."""
|
|
70
72
|
tds_version: NotRequired[str]
|
|
@@ -76,11 +78,11 @@ class SybaseConnectionParams(TypedDict):
|
|
|
76
78
|
conv: NotRequired[Mapping[int | type[Any], Callable[..., Any]]]
|
|
77
79
|
"""Custom datatype conversion dictionary mapping TDS types to python types."""
|
|
78
80
|
read_only: NotRequired[bool]
|
|
79
|
-
"""
|
|
81
|
+
"""Native pymssql login intent; does not enforce ASE read-only permissions."""
|
|
80
82
|
pool_recycle_seconds: NotRequired[int]
|
|
81
83
|
"""Maximum age in seconds before a pooled connection is recycled."""
|
|
82
84
|
health_check_interval: NotRequired[float]
|
|
83
|
-
"""
|
|
85
|
+
"""Idle duration in seconds before a connection is checked on checkout."""
|
|
84
86
|
extra: NotRequired["dict[str, Any]"]
|
|
85
87
|
"""Additional driver connection arguments."""
|
|
86
88
|
|
sqlspec/adapters/sybase/core.py
CHANGED
|
@@ -58,7 +58,9 @@ __all__ = (
|
|
|
58
58
|
"split_schema_and_table",
|
|
59
59
|
)
|
|
60
60
|
|
|
61
|
-
DEFAULT_SYBASE_CONN_PROPERTIES: Final[str] =
|
|
61
|
+
DEFAULT_SYBASE_CONN_PROPERTIES: Final[str] = (
|
|
62
|
+
"SET CHAINED OFF; SET QUOTED_IDENTIFIER ON; SET ANSINULL ON; SET TEXTSIZE 2147483647;"
|
|
63
|
+
)
|
|
62
64
|
DEFAULT_SYBASE_TDS_VERSION: Final[str] = "5.0"
|
|
63
65
|
|
|
64
66
|
_ERROR_NUMBER_PATTERN: Final[re.Pattern[str]] = re.compile(r"(?:\(([-]?\d+)(?:,|\))|\bMsg\s+([-]?\d+)\b)")
|
|
@@ -342,11 +344,10 @@ def extract_error_number(exc: BaseException | None) -> int | None:
|
|
|
342
344
|
def build_connection_config(connection_config: "Mapping[str, Any]") -> "dict[str, Any]":
|
|
343
345
|
"""Build a normalized Sybase ASE connection configuration dictionary.
|
|
344
346
|
|
|
345
|
-
|
|
346
|
-
and
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
``tds_version`` validator.
|
|
347
|
+
Host connections default to port 5000. FreeTDS section aliases retain an
|
|
348
|
+
explicit port and a private marker for pool validation. Session properties,
|
|
349
|
+
autocommit and charset receive ASE defaults unless supplied by the caller.
|
|
350
|
+
The pool validates protocol configuration without modifying ``TDSVER``.
|
|
350
351
|
|
|
351
352
|
Args:
|
|
352
353
|
connection_config: Raw connection configuration mapping.
|
|
@@ -360,8 +361,19 @@ def build_connection_config(connection_config: "Mapping[str, Any]") -> "dict[str
|
|
|
360
361
|
for extra_key, extra_value in extra.items():
|
|
361
362
|
if extra_value is not None:
|
|
362
363
|
config.setdefault(str(extra_key), extra_value)
|
|
364
|
+
freetds_server = config.pop("freetds_server", None)
|
|
365
|
+
server_alias = config.pop("server_alias", None)
|
|
366
|
+
alias_flag = config.pop("is_server_alias", False)
|
|
367
|
+
is_server_alias = bool(config.pop("_sybase_server_alias", False) or alias_flag)
|
|
368
|
+
alias = freetds_server or server_alias
|
|
369
|
+
if alias:
|
|
370
|
+
config["server"] = alias
|
|
371
|
+
is_server_alias = True
|
|
363
372
|
config.setdefault("server", config.pop("host", "localhost"))
|
|
364
|
-
|
|
373
|
+
if is_server_alias:
|
|
374
|
+
config["_sybase_server_alias"] = True
|
|
375
|
+
else:
|
|
376
|
+
config.setdefault("port", 5000)
|
|
365
377
|
config.setdefault("charset", "UTF-8")
|
|
366
378
|
config.setdefault("autocommit", True)
|
|
367
379
|
config.setdefault("conn_properties", DEFAULT_SYBASE_CONN_PROPERTIES)
|
|
@@ -11,7 +11,11 @@ from sqlspec.data_dictionary import (
|
|
|
11
11
|
IndexMetadata,
|
|
12
12
|
MetadataCapabilityProfile,
|
|
13
13
|
MetadataResult,
|
|
14
|
+
MetadataSource,
|
|
15
|
+
ObjectIdentity,
|
|
14
16
|
TableMetadata,
|
|
17
|
+
dependency_edges_from_foreign_keys,
|
|
18
|
+
sort_dependencies,
|
|
15
19
|
)
|
|
16
20
|
from sqlspec.data_dictionary.dialects.sybase import (
|
|
17
21
|
SYBASE_CONFIG,
|
|
@@ -65,28 +69,56 @@ def build_column_metadata(rows: "Iterable[dict[str, Any]]") -> "list[ColumnMetad
|
|
|
65
69
|
table_name=str(r.get("table_name", "")),
|
|
66
70
|
column_name=str(r.get("column_name", "")),
|
|
67
71
|
data_type=str(r.get("data_type", "")),
|
|
68
|
-
is_nullable=
|
|
72
|
+
is_nullable=_catalog_flag(r.get("is_nullable", True)),
|
|
69
73
|
column_default=str(r["column_default"]) if r.get("column_default") is not None else None,
|
|
70
74
|
ordinal_position=int(r.get("ordinal_position", 1)),
|
|
71
75
|
max_length=int(r.get("max_length", 0)) if r.get("max_length") is not None else 0,
|
|
72
76
|
numeric_precision=int(r.get("numeric_precision", 0)) if r.get("numeric_precision") is not None else 0,
|
|
73
77
|
numeric_scale=int(r.get("numeric_scale", 0)) if r.get("numeric_scale") is not None else 0,
|
|
74
|
-
is_primary=
|
|
75
|
-
is_unique=
|
|
76
|
-
is_identity=
|
|
78
|
+
is_primary=_catalog_flag(r.get("is_primary")),
|
|
79
|
+
is_unique=_catalog_flag(r.get("is_unique")),
|
|
80
|
+
is_identity=_catalog_flag(r.get("is_identity")),
|
|
77
81
|
identity_generation=str(r["identity_generation"])
|
|
78
82
|
if r.get("identity_generation")
|
|
79
|
-
else ("ALWAYS" if r.get("is_identity") else None),
|
|
80
|
-
is_generated=
|
|
83
|
+
else ("ALWAYS" if _catalog_flag(r.get("is_identity")) else None),
|
|
84
|
+
is_generated=_catalog_flag(r.get("is_generated")),
|
|
81
85
|
generation_expression=str(r["generation_expression"])
|
|
82
86
|
if r.get("generation_expression") is not None
|
|
83
87
|
else None,
|
|
84
|
-
is_hidden=
|
|
88
|
+
is_hidden=_catalog_flag(r.get("is_hidden")),
|
|
89
|
+
)
|
|
90
|
+
for r in _merge_definition_rows(
|
|
91
|
+
rows, ("schema_name", "table_name", "column_name"), ("column_default", "generation_expression")
|
|
85
92
|
)
|
|
86
|
-
for r in rows
|
|
87
93
|
]
|
|
88
94
|
|
|
89
95
|
|
|
96
|
+
def _catalog_flag(value: object) -> bool:
|
|
97
|
+
if isinstance(value, str):
|
|
98
|
+
return value.strip().upper() in {"YES", "Y", "TRUE", "1"}
|
|
99
|
+
return bool(value)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _merge_definition_rows(
|
|
103
|
+
rows: "Iterable[dict[str, Any]]", identity_fields: "tuple[str, ...]", text_fields: "tuple[str, ...]"
|
|
104
|
+
) -> "list[dict[str, Any]]":
|
|
105
|
+
"""Assemble catalog definition chunks without changing their whitespace or multiplicity."""
|
|
106
|
+
chunk_fields = ("_definition_id", "_definition_number", "_definition_colid2", "_definition_colid")
|
|
107
|
+
grouped: dict[tuple[Any, ...], list[dict[str, Any]]] = {}
|
|
108
|
+
for row in rows:
|
|
109
|
+
key = tuple(row.get(field) for field in identity_fields)
|
|
110
|
+
grouped.setdefault(key, []).append(row)
|
|
111
|
+
result: list[dict[str, Any]] = []
|
|
112
|
+
for chunks in grouped.values():
|
|
113
|
+
ordered = sorted(chunks, key=lambda row: tuple(int(row.get(field) or 0) for field in chunk_fields))
|
|
114
|
+
merged = {field: value for field, value in ordered[0].items() if field not in chunk_fields}
|
|
115
|
+
for field in text_fields:
|
|
116
|
+
parts = [str(row[field]) for row in ordered if row.get(field) is not None]
|
|
117
|
+
merged[field] = "".join(parts) if parts else None
|
|
118
|
+
result.append(merged)
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
|
|
90
122
|
def build_index_metadata(rows: "Iterable[dict[str, Any]]") -> "list[IndexMetadata]":
|
|
91
123
|
"""Group per-column Sybase sysindexes rows into index metadata."""
|
|
92
124
|
indexes_map: dict[tuple[str, str, str], dict[str, Any]] = {}
|
|
@@ -198,7 +230,43 @@ class SybaseSyncDataDictionary(SyncDataDictionaryBase):
|
|
|
198
230
|
schema_type=TableMetadata,
|
|
199
231
|
),
|
|
200
232
|
)
|
|
201
|
-
|
|
233
|
+
rows = merge_sybase_table_lists(ordered_rows, all_rows)
|
|
234
|
+
table_rows = {
|
|
235
|
+
ObjectIdentity(
|
|
236
|
+
row["table_name"],
|
|
237
|
+
"table",
|
|
238
|
+
schema=row.get("schema_name"),
|
|
239
|
+
dialect="sybase",
|
|
240
|
+
source=MetadataSource.CATALOG,
|
|
241
|
+
): row
|
|
242
|
+
for row in rows
|
|
243
|
+
if row.get("table_name") and row.get("table_type", "TABLE").upper() != "VIEW"
|
|
244
|
+
}
|
|
245
|
+
if not table_rows:
|
|
246
|
+
return rows
|
|
247
|
+
identities = sorted(table_rows, key=lambda node: (node.schema or "", node.name))
|
|
248
|
+
edges = tuple(
|
|
249
|
+
edge
|
|
250
|
+
for edge in dependency_edges_from_foreign_keys(
|
|
251
|
+
self.get_foreign_keys(driver, schema=schema_name), dialect="sybase"
|
|
252
|
+
)
|
|
253
|
+
if edge.from_object in table_rows and edge.to_object in table_rows
|
|
254
|
+
)
|
|
255
|
+
dependencies: dict[ObjectIdentity, set[ObjectIdentity]] = {node: set() for node in identities}
|
|
256
|
+
for edge in edges:
|
|
257
|
+
dependencies[edge.from_object].add(edge.to_object)
|
|
258
|
+
ordered = sort_dependencies(identities, edges).ordered
|
|
259
|
+
levels: dict[ObjectIdentity, int] = {}
|
|
260
|
+
result: list[TableMetadata] = []
|
|
261
|
+
for node in ordered:
|
|
262
|
+
level = max((levels[parent] + 1 for parent in dependencies[node]), default=0)
|
|
263
|
+
levels[node] = level
|
|
264
|
+
result.append({**table_rows[node], "dependency_level": level, "level": level})
|
|
265
|
+
result.extend(
|
|
266
|
+
{**table_rows[node], "dependency_level": 0, "level": 0} for node in identities if node not in levels
|
|
267
|
+
)
|
|
268
|
+
result.extend(row for row in rows if row.get("table_type", "TABLE").upper() == "VIEW")
|
|
269
|
+
return result
|
|
202
270
|
|
|
203
271
|
def get_columns(
|
|
204
272
|
self, driver: "SybaseDriver", table: str | None = None, schema: str | None = None
|
|
@@ -255,14 +323,23 @@ class SybaseSyncDataDictionary(SyncDataDictionaryBase):
|
|
|
255
323
|
table_name = self.resolve_identifier(table) if table is not None else None
|
|
256
324
|
query = self.get_query("constraints", "by_table" if table_name else "by_schema")
|
|
257
325
|
rows = driver.select(query, schema_name=schema_name, table_name=table_name)
|
|
258
|
-
return MetadataResult(
|
|
326
|
+
return MetadataResult(
|
|
327
|
+
"constraints",
|
|
328
|
+
items=tuple(
|
|
329
|
+
_merge_definition_rows(
|
|
330
|
+
rows, ("schema_name", "table_name", "constraint_name", "constraint_type"), ("check_clause",)
|
|
331
|
+
)
|
|
332
|
+
),
|
|
333
|
+
)
|
|
259
334
|
|
|
260
335
|
def get_views(self, driver: "SybaseDriver", schema: str | None = None) -> MetadataResult:
|
|
261
336
|
"""Get Sybase ASE view metadata."""
|
|
262
337
|
schema_name = self.resolve_schema(schema)
|
|
263
338
|
query = self.get_query("views", "by_schema")
|
|
264
339
|
rows = driver.select(query, schema_name=schema_name, view_name=None)
|
|
265
|
-
return MetadataResult(
|
|
340
|
+
return MetadataResult(
|
|
341
|
+
"views", items=tuple(_merge_definition_rows(rows, ("schema_name", "view_name"), ("definition",)))
|
|
342
|
+
)
|
|
266
343
|
|
|
267
344
|
def get_objects(self, driver: "SybaseDriver", schema: "str | None" = None) -> MetadataResult:
|
|
268
345
|
"""Get tables, views, and procedures from the Sybase ASE catalog."""
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Sybase ASE event queue store with ASE-compatible DDL."""
|
|
2
2
|
|
|
3
3
|
from sqlspec.adapters.sybase.config import SybaseConfig
|
|
4
|
-
from sqlspec.adapters.sybase.core import escape_sql_literal,
|
|
4
|
+
from sqlspec.adapters.sybase.core import escape_sql_literal, format_identifier
|
|
5
5
|
from sqlspec.extensions.events import BaseEventQueueStore
|
|
6
6
|
|
|
7
7
|
__all__ = ("SybaseEventQueueStore",)
|
|
@@ -57,10 +57,10 @@ class SybaseEventQueueStore(BaseEventQueueStore[SybaseConfig]):
|
|
|
57
57
|
)
|
|
58
58
|
|
|
59
59
|
def _wrap_create_statement(self, statement: str, object_type: str) -> str:
|
|
60
|
-
object_name = escape_sql_literal(
|
|
60
|
+
object_name = escape_sql_literal(format_identifier(self.table_name))
|
|
61
61
|
escaped_statement = escape_sql_literal(statement.rstrip().rstrip(";"))
|
|
62
62
|
if object_type == "table":
|
|
63
|
-
return f"IF OBJECT_ID('{object_name}'
|
|
63
|
+
return f"IF OBJECT_ID('{object_name}') IS NULL EXECUTE('{escaped_statement}')"
|
|
64
64
|
if object_type == "index":
|
|
65
65
|
index_name = escape_sql_literal(self._index_name())
|
|
66
66
|
return (
|
|
@@ -70,11 +70,6 @@ class SybaseEventQueueStore(BaseEventQueueStore[SybaseConfig]):
|
|
|
70
70
|
return statement
|
|
71
71
|
|
|
72
72
|
def _wrap_drop_statement(self, statement: str) -> str:
|
|
73
|
-
object_name = escape_sql_literal(
|
|
73
|
+
object_name = escape_sql_literal(format_identifier(self.table_name))
|
|
74
74
|
escaped_statement = escape_sql_literal(statement.rstrip().rstrip(";"))
|
|
75
|
-
return f"IF OBJECT_ID('{object_name}'
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def _object_name(table_name: str) -> str:
|
|
79
|
-
schema_name, bare_table_name = split_schema_and_table(table_name)
|
|
80
|
-
return f"{quote_bracket_identifier(schema_name)}.{quote_bracket_identifier(bare_table_name)}"
|
|
75
|
+
return f"IF OBJECT_ID('{object_name}') IS NOT NULL EXECUTE('{escaped_statement}');"
|
|
@@ -83,7 +83,7 @@ class SybaseStore(BaseSQLSpecStore["SybaseConfig"]):
|
|
|
83
83
|
)
|
|
84
84
|
create_index_sql = escape_sql_literal(f"CREATE INDEX {index_name} ON {table_ref}(expires_at)")
|
|
85
85
|
return (
|
|
86
|
-
f"IF OBJECT_ID('{escaped_table_ref}'
|
|
86
|
+
f"IF OBJECT_ID('{escaped_table_ref}') IS NULL EXECUTE('{create_table_sql}');\n"
|
|
87
87
|
f"IF NOT EXISTS (SELECT 1 FROM dbo.sysindexes WHERE name = '{escaped_index_bare}' "
|
|
88
88
|
f"AND id = OBJECT_ID('{escaped_table_ref}')) EXECUTE('{create_index_sql}');"
|
|
89
89
|
)
|
|
@@ -92,7 +92,7 @@ class SybaseStore(BaseSQLSpecStore["SybaseConfig"]):
|
|
|
92
92
|
"""Get Sybase ASE DROP TABLE statements."""
|
|
93
93
|
table_ref = _table_ref(self._table_name)
|
|
94
94
|
escaped = escape_sql_literal(table_ref)
|
|
95
|
-
return [f"IF OBJECT_ID('{escaped}'
|
|
95
|
+
return [f"IF OBJECT_ID('{escaped}') IS NOT NULL EXECUTE('DROP TABLE {escaped}');"]
|
|
96
96
|
|
|
97
97
|
def _create_table(self) -> None:
|
|
98
98
|
with self._config.provide_session() as driver:
|
|
@@ -55,7 +55,7 @@ class SybaseMigrationTrackerMixin:
|
|
|
55
55
|
"""Wrap CREATE TABLE in a Sybase ASE OBJECT_ID existence probe with dynamic EXECUTE."""
|
|
56
56
|
table_ref = escape_sql_literal(format_identifier(self.version_table))
|
|
57
57
|
create_sql = escape_sql_literal(self._tracking_table_ddl_text().rstrip().rstrip(";"))
|
|
58
|
-
return f"IF OBJECT_ID('{table_ref}'
|
|
58
|
+
return f"IF OBJECT_ID('{table_ref}') IS NULL EXECUTE('{create_sql}');"
|
|
59
59
|
|
|
60
60
|
def _tracking_table_ddl_text(self) -> str:
|
|
61
61
|
"""Render CREATE TABLE text with Sybase ASE native column types."""
|