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.
Files changed (42) hide show
  1. sqlspec/adapters/oracledb/driver.py +8 -3
  2. sqlspec/adapters/oracledb/events/backend.py +3 -2
  3. sqlspec/adapters/psqlpy/events/backend.py +3 -2
  4. sqlspec/adapters/psycopg/events/backend.py +5 -4
  5. sqlspec/adapters/sybase/adk/store.py +25 -22
  6. sqlspec/adapters/sybase/config.py +8 -6
  7. sqlspec/adapters/sybase/core.py +19 -7
  8. sqlspec/adapters/sybase/data_dictionary.py +88 -11
  9. sqlspec/adapters/sybase/events/store.py +5 -10
  10. sqlspec/adapters/sybase/litestar/store.py +2 -2
  11. sqlspec/adapters/sybase/migrations.py +1 -1
  12. sqlspec/adapters/sybase/pool.py +17 -11
  13. sqlspec/cli.py +6 -12
  14. sqlspec/config.py +5 -0
  15. sqlspec/core/result/_base.py +8 -2
  16. sqlspec/data_dictionary/dialects/sybase/sql/columns.sql +26 -12
  17. sqlspec/data_dictionary/dialects/sybase/sql/constraints.sql +39 -49
  18. sqlspec/data_dictionary/dialects/sybase/sql/foreign_keys.sql +6 -2
  19. sqlspec/data_dictionary/dialects/sybase/sql/views.sql +7 -3
  20. sqlspec/dialects/sybase/__init__.py +1 -1
  21. sqlspec/driver/_async.py +1 -0
  22. sqlspec/driver/_storage_helpers.py +81 -19
  23. sqlspec/driver/_sync.py +1 -0
  24. sqlspec/extensions/adk/_config_utils.py +44 -13
  25. sqlspec/extensions/adk/memory/store.py +1 -0
  26. sqlspec/extensions/adk/migrations/0002_create_mssql_python_memory.py +2 -1
  27. sqlspec/extensions/adk/store.py +1 -0
  28. sqlspec/extensions/events/_channel.py +32 -21
  29. sqlspec/extensions/events/_hints.py +2 -11
  30. sqlspec/extensions/events/_store.py +4 -1
  31. sqlspec/extensions/events/migrations/0001_create_event_queue.py +14 -9
  32. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +18 -11
  33. sqlspec/extensions/litestar/store.py +4 -1
  34. sqlspec/observability/_common.py +2 -0
  35. sqlspec/storage/_arrow_payload.py +3 -1
  36. sqlspec/utils/adapters.py +186 -0
  37. sqlspec/utils/arrow_helpers.py +34 -26
  38. {sqlspec-0.70.0.dist-info → sqlspec-0.71.0.dist-info}/METADATA +1 -1
  39. {sqlspec-0.70.0.dist-info → sqlspec-0.71.0.dist-info}/RECORD +42 -41
  40. {sqlspec-0.70.0.dist-info → sqlspec-0.71.0.dist-info}/WHEEL +1 -1
  41. {sqlspec-0.70.0.dist-info → sqlspec-0.71.0.dist-info}/entry_points.txt +0 -0
  42. {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
- yield from batch_table.to_batches()
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" not in type(config).__module__:
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" not in type(config).__module__:
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" not in type(config).__module__:
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" not in type(config).__module__:
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 "psycopg" not in type(config).__module__:
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 "psycopg" not in type(config).__module__:
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 "psycopg" not in type(config).__module__:
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 "psycopg" not in type(config).__module__:
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}', 'U') IS NOT NULL EXECUTE('DROP TABLE {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}', 'U') IS NOT NULL EXECUTE('DROP TABLE {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}', 'U') IS NOT NULL EXECUTE('DROP TABLE {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}', 'U') IS NOT NULL EXECUTE('DROP TABLE {events_ref}')",
391
- f"IF OBJECT_ID('{sessions_ref}', 'U') IS NOT NULL EXECUTE('DROP TABLE {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}%", limit_value))
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}', 'U') IS NULL EXECUTE('{create_sql}');"
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}', 'U') IS NOT NULL EXECUTE('DROP TABLE {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}', 'U') IS NULL EXECUTE('{create_sql}');"
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}', 'U') IS NULL EXECUTE('{create_sql}');"
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}', 'U') IS NULL EXECUTE('{create_sql}');"
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}', 'U') IS NULL EXECUTE('{create_sql}');"
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}', 'U') IS NULL EXECUTE('{create_sql}');"
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
- limit_clause = ""
796
+ top_clause = ""
798
797
  if limit is not None:
799
- limit_clause = "\n ROWS LIMIT %s"
800
- params.append(limit)
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{limit_clause}
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
- """Alias for ``server``."""
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 of the Sybase ASE server."""
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
- """Additional connection properties string."""
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
- """Enforce read-only transactions on connections."""
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
- """Interval in seconds between background health checks."""
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
 
@@ -58,7 +58,9 @@ __all__ = (
58
58
  "split_schema_and_table",
59
59
  )
60
60
 
61
- DEFAULT_SYBASE_CONN_PROPERTIES: Final[str] = "SET QUOTED_IDENTIFIER ON; SET ANSINULL ON; SET TEXTSIZE 2147483647;"
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
- Injects ``DEFAULT_SYBASE_CONN_PROPERTIES``, ``autocommit=True``, ``port=5000``,
346
- and ``charset='UTF-8'`` by default, and strips ``tds_version`` into
347
- ``_sybase_tds_version`` so ``SybaseConnectionPool`` can negotiate TDS 5.0
348
- via ``os.environ['TDSVER']`` without triggering ``pymssql._mssql``'s Cython
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
- config.setdefault("port", 5000)
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=bool(r.get("is_nullable", True)),
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=bool(r.get("is_primary")),
75
- is_unique=bool(r.get("is_unique")),
76
- is_identity=bool(r.get("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=bool(r.get("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=bool(r.get("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
- return merge_sybase_table_lists(ordered_rows, all_rows)
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("constraints", items=tuple(rows))
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("views", items=tuple(rows))
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, quote_bracket_identifier, split_schema_and_table
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(_object_name(self.table_name))
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}', 'U') IS NULL EXECUTE('{escaped_statement}')"
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(_object_name(self.table_name))
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}', 'U') IS NOT NULL EXECUTE('{escaped_statement}');"
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}', 'U') IS NULL EXECUTE('{create_table_sql}');\n"
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}', 'U') IS NOT NULL EXECUTE('DROP TABLE {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}', 'U') IS NULL EXECUTE('{create_sql}');"
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."""