Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ac6d295
test(fixtures): isolate root pytest plugins and migrate db2 to client…
cofin Oct 25, 2026
aaace8f
test(fixtures): modernize rustfs, gizmosql, bigquery, and spanner cli…
cofin Oct 25, 2026
665062e
test(fixtures): decouple cross-driver fixtures and guard optional ada…
cofin Oct 25, 2026
9fd104d
test(mysql): consolidate adk residual suite across all mysql drivers
cofin Oct 25, 2026
77a2ded
test(storage): provision rustfs bucket via stdlib sigv4 and decouple …
cofin Oct 25, 2026
7116ca4
test(xdist): preserve distinct container xdist groups and fix adbc fa…
cofin Oct 25, 2026
afca502
ci(integration): shard integration matrix across 5 parallel service g…
cofin Oct 25, 2026
6fbd427
build(makefile): parallelize integration tests and add per-shard targets
cofin Oct 25, 2026
dd8a7e6
test(xdist): strip ancestor xdist_group markers and isolate secondary…
cofin Oct 25, 2026
023ae81
chore(test): add cloud storage and pure-go adbc drivers to extras group
cofin Oct 25, 2026
26817a2
test(oracle): add live adb-free 23ai mtls wallet_zip and dbms_cloud s…
cofin Oct 25, 2026
e17ef7d
test(storage): add multi-cloud floci/azurite storage, streaming, and …
cofin Oct 25, 2026
4a58c08
fix(postgres): use enable_columnar_scan GUC and add alloydb omni 18 a…
cofin Oct 26, 2026
4e889a3
test(adbc): wire containerized oracle, db2, snowflake, and bigquery a…
cofin Oct 26, 2026
f7856dd
ci(integration): wire unlocked container images and snowflake shard t…
cofin Oct 26, 2026
01b61c6
fix(postgres,tests): preserve floci HEAD content-length and align aut…
cofin Oct 26, 2026
3241836
docs(adapters): update contributor and adapter testing guides
cofin Oct 26, 2026
3906676
fix(adapters): handle gizmosql adbc defaults and python 3.13 oracle s…
cofin Oct 26, 2026
052a7ca
test(oracledb): satisfy mypy func-returns-value in wallet ssl_context…
cofin Oct 27, 2026
b0af1b1
fix(duckdb,fixtures): use range nextval loop for sequence resynchroni…
cofin Oct 30, 2026
b99fdec
fix(duckdb,tests): restore literal destination URI for native copy st…
cofin Oct 30, 2026
cfc18aa
fix(testing): overhaul clientless test infrastructure and database sh…
cofin Oct 31, 2026
7945fc6
test(mysql): allow driver-native integrity errors in adk owner_id con…
cofin Oct 31, 2026
ebf1cbd
feat(dialects): add sybase and sqlanywhere sqlglot dialects with hot-…
cofin Oct 30, 2026
e1da037
feat(data-dictionary): add sybase and sqlanywhere catalog queries and…
cofin Oct 30, 2026
2dfafec
feat(sybase): add sap ase adapter over pymssql tds 5.0 and extension …
cofin Oct 30, 2026
4f24e42
feat(sqlanywhere): add sap sql anywhere adapter over sqlanydb and ext…
cofin Oct 30, 2026
4d438bf
build(mypyc): register sybase and sqlanywhere hot-path modules in myp…
cofin Oct 30, 2026
061c022
feat(data-dictionary,utils): align sybase and sqlanywhere fixtures an…
cofin Oct 31, 2026
d256a0e
fix(dialects): correct sybase and sqlanywhere pagination, timestamps,…
cofin Oct 31, 2026
8cb14a2
feat(data-dictionary): rewrite sybase and sqlanywhere catalog queries…
cofin Oct 31, 2026
738d0c2
fix(adapters): harden sybase and sqlanywhere stores, session rollback…
cofin Oct 31, 2026
2f297f5
docs(databases): document sybase and sqlanywhere database setup and d…
cofin Oct 31, 2026
74fc6be
docs(adapters): add field docstrings for sybase and sqlanywhere typed…
cofin Oct 31, 2026
5db9160
fix(adapters): eliminate eager pymssql import in sybase and sqlanywhe…
cofin Oct 31, 2026
113c941
Merge branch 'main' into research_sybase_adbc_driver
cofin Oct 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/databases/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ SQLSpec supports synchronous and asynchronous **database adapters** spanning emb
| [`oracledb`](oracledb.md) | `sqlspec[oracledb]` | Sync + Async | `:1` / `:name` | `ConnectionPool` / `AsyncConnectionPool` + DRCP | PyCapsule Batches | Oracle AQ (`DBMS_AQ`) + Table Queue | Sessions + Memory (Oracle 26ai `VECTOR` HNSW, Oracle Text, RRF) |
| [`mssql_python`](mssql.md) | `sqlspec[mssql-python]` | Sync | `?` (`qmark`) | `MssqlPythonConnectionPool` | Native `BulkCopy` | Table Queue | Sessions + Memory (`LIKE`) |
| [`pymssql`](mssql.md) | `sqlspec[pymssql]` | Sync | `%(name)s` / `%s` | `PymssqlConnectionPool` | Row | Table Queue | Sessions + Memory (`LIKE`) |
| [`sybase`](sybase.md) | `sqlspec[sybase]` | Sync | `?` (`qmark`) / `%(name)s` | `SybaseConnectionPool` | Row / Stream | Table Queue | Sessions + Memory (`LIKE`) |
| [`sqlanywhere`](sqlanywhere.md) | `sqlspec[sqlanywhere]` | Sync | `?` (`qmark`) | `SqlanywhereConnectionPool` | Row / Stream | Table Queue | Sessions + Memory (`LIKE`) |
| [`db2`](db2.md) | `sqlspec[db2]` | Sync + Async | `?` (`qmark`) | `Db2SyncConnectionPool` / `Db2AsyncConnectionPool` | Schema-Guided Row | Table Queue (`SKIP LOCKED DATA`) | Sessions + Memory (`POSSTR`) |
| [`duckdb`](duckdb.md) | `sqlspec[duckdb]` | Sync | `?` (`qmark`) | `DuckDBConnectionPool` | Native Zero-Copy | Table Queue | Sessions + Memory (`FTS`, `ARRAY` Cosine) |
| [`bigquery`](bigquery.md) | `sqlspec[bigquery]` | Sync | `@name` (`named`) | Stateless HTTP/gRPC Client | Storage Read / Write API | Table Queue | Sessions + Memory (`SEARCH` / `LIKE`) |
Expand All @@ -35,6 +37,8 @@ SQLSpec supports synchronous and asynchronous **database adapters** spanning emb
- **[MySQL and MariaDB](mysql.md)**: Side-by-side setup for `asyncmy`, `pymysql`, `aiomysql`, and `mysql-connector-python`, TLS verification, and `LOAD DATA LOCAL INFILE` bulk loading.
- **[Oracle Database](oracledb.md)**: Thin and Thick modes, mTLS/PKCS#12 wallets, TNS topologies, Oracle 26ai `VECTOR` + Hybrid RRF search, PyCapsule Arrow streaming, and `DBMS_CLOUD.COPY_DATA`.
- **[Microsoft SQL Server](mssql.md)**: Microsoft's official `mssql-python` driver with streaming Arrow `BulkCopy` alongside FreeTDS `pymssql`.
- **[SAP Adaptive Server Enterprise](sybase.md)**: Sybase ASE 16.0+ via FreeTDS TDS 5.0 (`pymssql`), transactional DDL behavior, and catalog introspection.
- **[SAP SQL Anywhere](sqlanywhere.md)**: Embedded and client-server SQL Anywhere 12+ via `sqlanydb`, recursive dependency analysis, and automatic rollback on context exit.
- **[IBM Db2](db2.md)**: Sync and async DRDA pooling (`Db2SyncConfig` / `Db2AsyncConfig`), savepoint cursor retention, and lowercase column normalization.
- **[DuckDB](duckdb.md)**: In-process OLAP queries over Parquet/S3, hybrid native/Arrow materialization, and JSON statement profiling.
- **[Google Cloud BigQuery](bigquery.md)**: Interactive `query_and_wait` execution, Storage Read/Write APIs, reservation governance, and multi-statement script jobs.
Expand Down
53 changes: 53 additions & 0 deletions docs/databases/sqlanywhere.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# SAP SQL Anywhere

`sqlspec.adapters.sqlanywhere` provides synchronous database connectivity for SAP SQL Anywhere 12+ through the official `sqlanydb` driver (`>= 1.0.14`), offering connection pooling (`SqlanywhereConfig`), custom SQLGlot `sqlanywhere` dialect compilation, recursive foreign-key dependency analysis, and Litestar, Events, and Google ADK session/memory store extensions.

## Install and When to Choose

```bash
uv add "sqlspec[sqlanywhere]"
```

- Choose **`SqlanywhereConfig`** for embedded, edge, and client-server applications interacting with SAP SQL Anywhere database engines.
- `sqlanydb` wraps SAP's native client C library (`dbcapi`) via `ctypes`. Ensure `libdbcapi_r.so` (Linux), `libdbcapi_r.dylib` (macOS), or `dbcapi.dll` (Windows) is installed and available in the system library path.

## Minimal Connection and Pool Setup

```python
from sqlspec import SQLSpec
from sqlspec.adapters.sqlanywhere import SqlanywhereConfig

spec = SQLSpec()
config = spec.add_config(
SqlanywhereConfig(
connection_config={
"uid": "dba",
"pwd": "sql",
"eng": "demo_server",
"dbn": "demo",
"host": "localhost:2638",
}
)
)
```

## Key Configuration Parameters

| Setting | Location | Description |
| --- | --- | --- |
| `uid` / `user` | `connection_config` | Database user name (default DBA administrative principal). |
| `pwd` / `password` | `connection_config` | Database user password. |
| `eng` / `server` | `connection_config` | Running SQL Anywhere database engine name. |
| `dbn` / `database` | `connection_config` | Target database catalog name. |
| `host` | `connection_config` | Network host and port string (`host:port`, default port 2638). |
| `dbf` | `connection_config` | Direct database file path for auto-starting engines. |
| `pool_recycle_seconds` | `connection_config` | Maximum connection age in seconds before recycling. |
| `health_check_interval` | `connection_config` | Idle duration in seconds before running ping probes. |
| `json_serializer` / `json_deserializer` | `driver_features` | Custom JSON serialization/deserialization callables. |

## Dialect and Catalog Notes

- **Automatic Rollback on Context Exit**: SQL Anywhere connections maintain an implicit transaction upon statement execution. `SqlanywhereSessionContext` automatically rolls back uncommitted changes if a session exits with open work or an error, ensuring clean connection return to the pool.
- **Pagination**: Supports standard suffix `SELECT ... LIMIT n OFFSET m` as well as SQL Anywhere `TOP n START AT (m + 1)` syntax.
- **Identity Resync**: Fixture seeding supports primary key identity resetting via `CALL sa_reset_identity('<table>', '<schema>', <max_id>)`.
- **System Catalogs**: Schema discovery queries `SYS.SYSTAB`, `SYS.SYSTABCOL`, `SYS.SYSIDX`, `SYS.SYSIDXCOL`, `SYS.SYSFKEY`, and `SYS.SYSDOMAIN`.
82 changes: 82 additions & 0 deletions docs/databases/sybase.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# SAP Adaptive Server Enterprise (Sybase ASE)

`sqlspec.adapters.sybase` provides first-class support for SAP Adaptive Server Enterprise (SAP ASE) 16.0+ via `pymssql` (`>= 2.4.4`) over FreeTDS TDS 5.0 wire protocol, featuring connection pooling (`SybaseConfig`), custom SQLGlot `sybase` dialect compilation, system catalog introspection, and Litestar, Events, and Google ADK session/memory store extensions.

## Install and When to Choose

```bash
uv add "sqlspec[sybase]"
```

- Choose **`SybaseConfig`** for transactional OLTP applications communicating with SAP ASE database instances.
- Prebuilt `pymssql` wheels ship statically linked FreeTDS with native TDS 5.0 wire support across Linux, macOS, and Windows.
- Note: Neither Apache Arrow ADBC nor GizmoData provides native ADBC drivers for SAP ASE, so SQLSpec communicates with ASE directly via FreeTDS TDS 5.0 DB-API connections.

## Connection Setup and Protocol Negotiation

Sybase ASE requires TDS 5.0 protocol negotiation. FreeTDS negotiates TDS 5.0 either when an environment variable `TDSVER=5.0` is set before process start, or when a FreeTDS server section alias is defined in `freetds.conf`:

=== "Environment Variable (`TDSVER=5.0`)"

```bash
export TDSVER=5.0
```

```python
from sqlspec import SQLSpec
from sqlspec.adapters.sybase import SybaseConfig

spec = SQLSpec()
config = spec.add_config(
SybaseConfig(
connection_config={
"server": "ase.example.internal",
"port": 5000,
"user": "sa",
"password": "secret_password",
"database": "app_db",
"autocommit": True,
}
)
)
```

=== "FreeTDS Server Alias (`server_alias`)"

```python
from sqlspec import SQLSpec
from sqlspec.adapters.sybase import SybaseConfig

spec = SQLSpec()
config = spec.add_config(
SybaseConfig(
connection_config={
"server_alias": "SYBASE_PROD",
"user": "sa",
"password": "secret_password",
"database": "app_db",
}
)
)
```

## Key Configuration Parameters

| Setting | Location | Description |
| --- | --- | --- |
| `server` / `host` | `connection_config` | Database host name or IP address (default port `5000`). |
| `server_alias` / `freetds_server` | `connection_config` | Section name in `freetds.conf` configured with `tds version = 5.0`. |
| `database` / `db` | `connection_config` | Target database catalog name. |
| `user` / `username` | `connection_config` | Authentication login principal. |
| `password` | `connection_config` | Authentication password credentials. |
| `autocommit` | `connection_config` | Defaults to `True` to allow DDL execution outside implicit transactions. |
| `conn_properties` | `connection_config` | Sybase session initialization commands (defaults to quoted identifier and ANSINULL options). |
| `pool_recycle_seconds` | `connection_config` | Maximum connection age in seconds before recycling (defaults to 24 hours). |
| `health_check_interval` | `connection_config` | Minimum idle time in seconds before running liveness probes. |
| `json_serializer` / `json_deserializer` | `driver_features` | Custom JSON text serialization/deserialization callables. |

## Dialect and Catalog Notes

- **Autocommit and DDL in Transactions**: In Sybase ASE, statements like `CREATE TABLE` and `CREATE INDEX` raise error 2762 inside multi-statement transactions unless database option `ddl in tran` is enabled. `SybaseConfig` defaults `autocommit=True` and begins explicit transactions on demand via `driver.begin()`.
- **Pagination**: Supports both unparenthesized `SELECT TOP n ...` and ANSI SQL pagination `ROWS LIMIT n OFFSET m`.
- **System Catalogs**: Reflection queries introspect `dbo.sysobjects`, `dbo.syscolumns`, `dbo.sysindexes`, `dbo.sysreferences`, and `dbo.systypes`.
11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ pymssql = ["pymssql"]
pymysql = ["pymysql"]
sanic = ["sanic"]
spanner = ["google-cloud-spanner>=3.64.0"]
sqlanywhere = ["sqlanydb>=1.0.14"]
starlette = ["starlette"]
sybase = ["pymssql"]
uuid = ["uuid-utils>=0.17.0"]

[dependency-groups]
Expand Down Expand Up @@ -156,6 +158,8 @@ pg_textsearch = "sqlspec.dialects.postgres:PGTextSearch"
pgvector = "sqlspec.dialects.postgres:PGVector"
spangres = "sqlspec.dialects.spanner:Spangres"
spanner = "sqlspec.dialects.spanner:Spanner"
sqlanywhere = "sqlspec.dialects.sybase:SQLAnywhere"
sybase = "sqlspec.dialects.sybase:Sybase"

[tool.uv]
exclude-newer = "7 days"
Expand Down Expand Up @@ -260,6 +264,9 @@ include = [
"sqlspec/dialects/db2/_generators.py", # Db2 custom dialect render hooks
"sqlspec/dialects/db2/_parsers.py", # Db2 custom dialect parser hooks
"sqlspec/dialects/db2/_transforms.py", # Db2 custom dialect transform helpers
"sqlspec/dialects/sybase/_generators.py", # Sybase and SQL Anywhere custom dialect render hooks
"sqlspec/dialects/sybase/_parsers.py", # Sybase and SQL Anywhere custom dialect parser hooks
"sqlspec/dialects/sybase/_transforms.py", # Sybase and SQL Anywhere custom dialect transform helpers
"sqlspec/storage/registry.py", # Safe storage registry/runtime routing
"sqlspec/storage/errors.py", # Safe storage error normalization
"sqlspec/storage/_paths.py", # Pure storage path handling
Expand Down Expand Up @@ -484,6 +491,8 @@ markers = [
"mysql_connector: marks tests using mysql-connector",
"pymysql: marks tests using pymysql",
"psqlpy: marks tests using psqlpy",
"sqlanywhere: marks tests using the sqlanywhere adapter",
"sybase: marks tests using the sybase adapter",
]
python_files = ["test_*.py"]
testpaths = ["tests"]
Expand Down Expand Up @@ -539,6 +548,8 @@ module = [
"ibm_db.*",
"ibm_db_dbi",
"ibm_db_dbi.*",
"sqlanydb",
"sqlanydb.*",
"sqlglot",
"sqlglot.*",
]
Expand Down
40 changes: 40 additions & 0 deletions sqlspec/adapters/sqlanywhere/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""SAP SQL Anywhere adapter for SQLSpec."""

from sqlspec.adapters.sqlanywhere._typing import SqlanywhereConnection, SqlanywhereCursor
from sqlspec.adapters.sqlanywhere.config import (
SqlanywhereConfig,
SqlanywhereConnectionParams,
SqlanywhereDriverFeatures,
SqlanywherePoolParams,
)
from sqlspec.adapters.sqlanywhere.core import (
DEFAULT_SQLANYWHERE_PORT,
build_connection_config,
create_mapped_exception,
default_statement_config,
driver_profile,
)
from sqlspec.adapters.sqlanywhere.data_dictionary import SqlanywhereDataDictionary, SqlanywhereSyncDataDictionary
from sqlspec.adapters.sqlanywhere.driver import SqlanywhereDriver, SqlanywhereExceptionHandler
from sqlspec.adapters.sqlanywhere.migrations import SqlanywhereSyncMigrationTracker
from sqlspec.adapters.sqlanywhere.pool import SqlanywhereConnectionPool

__all__ = (
"DEFAULT_SQLANYWHERE_PORT",
"SqlanywhereConfig",
"SqlanywhereConnection",
"SqlanywhereConnectionParams",
"SqlanywhereConnectionPool",
"SqlanywhereCursor",
"SqlanywhereDataDictionary",
"SqlanywhereDriver",
"SqlanywhereDriverFeatures",
"SqlanywhereExceptionHandler",
"SqlanywherePoolParams",
"SqlanywhereSyncDataDictionary",
"SqlanywhereSyncMigrationTracker",
"build_connection_config",
"create_mapped_exception",
"default_statement_config",
"driver_profile",
)
123 changes: 123 additions & 0 deletions sqlspec/adapters/sqlanywhere/_typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""SAP SQL Anywhere adapter type definitions.

This module contains type aliases and classes that are excluded from mypyc
compilation to avoid ABI boundary issues.
"""

import contextlib
from typing import TYPE_CHECKING, Any

from mypy_extensions import mypyc_attr

from sqlspec.utils.module_loader import import_optional_attr

SQLANYDB_MODULE: Any = None
_DRIVER_CLS: Any = None


class SqlanywhereError(Exception):
"""SAP SQL Anywhere driver exception base."""


if TYPE_CHECKING:
from collections.abc import Callable
from types import TracebackType
from typing import TypeAlias

from sqlspec.adapters.sqlanywhere.driver import SqlanywhereDriver
from sqlspec.core import StatementConfig

SqlanywhereConnection: TypeAlias = Any
SqlanywhereRawCursor: TypeAlias = Any
SqlanywhereQueryParams: TypeAlias = Any

if not TYPE_CHECKING:
SqlanywhereConnection = Any
SqlanywhereRawCursor = Any
SqlanywhereQueryParams = Any

__all__ = (
"SQLANYDB_MODULE",
"SqlanywhereConnection",
"SqlanywhereCursor",
"SqlanywhereError",
"SqlanywhereQueryParams",
"SqlanywhereRawCursor",
"SqlanywhereSessionContext",
)


class SqlanywhereCursor:
"""Context manager for SQL Anywhere sqlanydb cursor operations."""

__slots__ = ("connection", "cursor")

def __init__(self, connection: "SqlanywhereConnection") -> None:
self.connection = connection
self.cursor: SqlanywhereRawCursor | None = None

def __enter__(self) -> "SqlanywhereRawCursor":
self.cursor = self.connection.cursor()
assert self.cursor is not None
return self.cursor

def __exit__(self, *_: Any) -> None:
if self.cursor is not None and hasattr(self.cursor, "close"):
with contextlib.suppress(Exception):
self.cursor.close()


@mypyc_attr(allow_interpreted_subclasses=True, native_class=False)
class SqlanywhereSessionContext:
"""Sync context manager for SAP SQL Anywhere sessions."""

__slots__ = (
"_acquire_connection",
"_connection",
"_driver",
"_driver_features",
"_prepare_driver",
"_release_connection",
"_statement_config",
)

def __init__(
self,
acquire_connection: "Callable[[], Any]",
release_connection: "Callable[..., Any]",
statement_config: "StatementConfig",
driver_features: "dict[str, Any]",
prepare_driver: "Callable[[SqlanywhereDriver], SqlanywhereDriver]",
) -> None:
self._acquire_connection = acquire_connection
self._release_connection = release_connection
self._statement_config = statement_config
self._driver_features = driver_features
self._prepare_driver = prepare_driver
self._connection: Any = None
self._driver: SqlanywhereDriver | None = None

def __enter__(self) -> "SqlanywhereDriver":
global _DRIVER_CLS
if _DRIVER_CLS is None:
_DRIVER_CLS = import_optional_attr("sqlspec.adapters.sqlanywhere.driver", "SqlanywhereDriver")
self._connection = self._acquire_connection()
self._driver = _DRIVER_CLS(
connection=self._connection, statement_config=self._statement_config, driver_features=self._driver_features
)
assert self._driver is not None
return self._prepare_driver(self._driver)

def __exit__(
self, exc_type: "type[BaseException] | None", exc_val: "BaseException | None", exc_tb: "TracebackType | None"
) -> "bool | None":
if self._connection is None:
return None
try:
if self._driver is not None:
self._driver._release_open_work()
finally:
self._release_connection(self._connection)
self._connection = None
self._driver = None
return None
5 changes: 5 additions & 0 deletions sqlspec/adapters/sqlanywhere/adk/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""SAP SQL Anywhere ADK store module."""

from sqlspec.adapters.sqlanywhere.adk.store import SqlanywhereADKConfig, SqlanywhereADKMemoryStore, SqlanywhereADKStore

__all__ = ("SqlanywhereADKConfig", "SqlanywhereADKMemoryStore", "SqlanywhereADKStore")
Loading
Loading