sqlspec 0.70.1__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.
@@ -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)
@@ -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."""
@@ -1,4 +1,4 @@
1
- """Sybase ASE connection pool with thread-locked TDSVER='5.0' negotiation."""
1
+ """Sybase ASE thread-local connection pool with TDS 5.0 configuration validation."""
2
2
 
3
3
  import contextlib
4
4
  import logging
@@ -9,7 +9,7 @@ from contextlib import contextmanager
9
9
  from typing import TYPE_CHECKING, Any, cast
10
10
 
11
11
  from sqlspec.adapters.sybase._typing import PYMSSQL_MODULE, SybaseConnection
12
- from sqlspec.adapters.sybase.core import DEFAULT_SYBASE_CONN_PROPERTIES, DEFAULT_SYBASE_TDS_VERSION
12
+ from sqlspec.adapters.sybase.core import DEFAULT_SYBASE_TDS_VERSION, build_connection_config
13
13
  from sqlspec.exceptions import ImproperConfigurationError
14
14
  from sqlspec.utils.logging import POOL_LOGGER_NAME, get_logger, log_with_context
15
15
  from sqlspec.utils.uuids import uuid4
@@ -51,15 +51,19 @@ class SybaseConnectionPool:
51
51
  """Initialize the thread-local Sybase ASE connection manager.
52
52
 
53
53
  Args:
54
- connection_parameters: Normalized Sybase connection parameters.
54
+ connection_parameters: Raw or normalized Sybase connection parameters.
55
55
  recycle_seconds: Connection recycle time in seconds (default 24h).
56
56
  health_check_interval: Seconds of idle time before running health check.
57
57
  on_connection_create: Callback executed when a connection is created.
58
58
  """
59
- params = dict(connection_parameters)
60
- freetds_alias = params.get("freetds_server") or params.get("server_alias") or params.get("is_server_alias")
61
- if freetds_alias and isinstance(freetds_alias, str):
62
- params["server"] = freetds_alias
59
+ params = build_connection_config(connection_parameters)
60
+ freetds_alias = params.pop("_sybase_server_alias", False)
61
+ if freetds_alias and params.get("port") in (None, "", 0, "0"):
62
+ msg = (
63
+ "FreeTDS server aliases require an explicit nonzero port: "
64
+ "pymssql overrides the port configured in freetds.conf."
65
+ )
66
+ raise ImproperConfigurationError(msg)
63
67
  tdsver = os.environ.get("TDSVER")
64
68
  if not freetds_alias and tdsver != "5.0":
65
69
  msg = (
@@ -71,8 +75,6 @@ class SybaseConnectionPool:
71
75
  self._tds_version = str(
72
76
  params.pop("_sybase_tds_version", params.pop("tds_version", DEFAULT_SYBASE_TDS_VERSION))
73
77
  )
74
- params.setdefault("conn_properties", DEFAULT_SYBASE_CONN_PROPERTIES)
75
- params.setdefault("autocommit", True)
76
78
  self._connection_parameters = params
77
79
  self._thread_local = threading.local()
78
80
  self._connection_registry: set[SybaseConnection] = set()
@@ -110,14 +112,18 @@ class SybaseConnectionPool:
110
112
  Yields:
111
113
  A thread-local database connection.
112
114
  """
115
+ depth = getattr(self._thread_local, "depth", 0)
113
116
  connection = self.acquire()
117
+ self._thread_local.depth = depth + 1
114
118
  try:
115
119
  yield connection
116
120
  except Exception:
117
- with contextlib.suppress(Exception):
118
- self._close_thread_connection()
121
+ if getattr(self._thread_local, "depth", 1) <= 1:
122
+ with contextlib.suppress(Exception):
123
+ self._close_thread_connection()
119
124
  raise
120
125
  finally:
126
+ self._thread_local.depth = max(0, getattr(self._thread_local, "depth", 1) - 1)
121
127
  self.release(connection)
122
128
 
123
129
  def close(self) -> None:
@@ -994,7 +994,10 @@ class ArrowResult(StatementResult):
994
994
  raise ValueError(msg)
995
995
  active_pipeline = pipeline or SyncStoragePipeline()
996
996
  if hasattr(self.data, "to_batches") or hasattr(self.data, "iter_batches") or hasattr(self.data, "__iter__"):
997
- _cols, batch_iter = coerce_arrow_batches(self.data)
997
+ if hasattr(self.data, "__arrow_c_stream__"):
998
+ batch_iter = self.data
999
+ else:
1000
+ _cols, batch_iter = coerce_arrow_batches(self.data)
998
1001
  return active_pipeline.write_arrow_batches(
999
1002
  destination,
1000
1003
  batch_iter,
@@ -1021,7 +1024,10 @@ class ArrowResult(StatementResult):
1021
1024
  raise ValueError(msg)
1022
1025
  active_pipeline = pipeline or AsyncStoragePipeline()
1023
1026
  if hasattr(self.data, "to_batches") or hasattr(self.data, "iter_batches") or hasattr(self.data, "__iter__"):
1024
- _cols, batch_iter = coerce_arrow_batches(self.data)
1027
+ if hasattr(self.data, "__arrow_c_stream__"):
1028
+ batch_iter = self.data
1029
+ else:
1030
+ _cols, batch_iter = coerce_arrow_batches(self.data)
1025
1031
  return await active_pipeline.write_arrow_batches_async(
1026
1032
  destination,
1027
1033
  batch_iter,