Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ maintainers = [{ name = "Litestar Developers", email = "hello@litestar.dev" }]
name = "sqlspec"
readme = "README.md"
requires-python = ">=3.10, <4.0"
version = "0.47.0"
version = "0.48.0"

[project.urls]
Discord = "https://discord.gg/litestar"
Expand Down Expand Up @@ -148,7 +148,7 @@ test = [
"pytest>=8.0.0",

"pytest-cov>=5.0.0",
"pytest-databases[postgres,oracle,bigquery,spanner]>=0.18.0",
"pytest-databases[postgres,oracle,bigquery,spanner]>=0.19.0",
"pytest-mock>=3.14.0",
"pytest-sugar>=1.0.0",
"pytest-xdist>=3.6.1",
Expand Down Expand Up @@ -192,6 +192,7 @@ packages = ["sqlspec"]
# Keep that smoke build from auto-discovering ignored scratch directories.
packages = []


[tool.hatch.build.targets.wheel.hooks.mypyc]
dependencies = ["hatch-mypyc", "hatch-cython", "mypy>=2.0.0"]
enable-by-default = false
Expand Down Expand Up @@ -291,7 +292,7 @@ opt_level = "3" # Maximum optimization (0-3)
allow_dirty = true
commit = false
commit_args = "--no-verify"
current_version = "0.47.0"
current_version = "0.48.0"
ignore_missing_files = false
ignore_missing_version = false
message = "chore(release): bump to v{new_version}"
Expand Down
7 changes: 6 additions & 1 deletion sqlspec/adapters/adbc/_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,16 @@ def __init__(
self._driver: AdbcDriver | None = None

def __enter__(self) -> "AdbcDriver":
from sqlspec.adapters.adbc.core import resolve_dialect_name
from sqlspec.adapters.adbc.driver import AdbcDriver

self._connection = self._acquire_connection()
resolved_dialect = resolve_dialect_name(self._statement_config.dialect) or None
self._driver = AdbcDriver(
connection=self._connection, statement_config=self._statement_config, driver_features=self._driver_features
connection=self._connection,
statement_config=self._statement_config,
driver_features=self._driver_features,
dialect=resolved_dialect,
)
return self._prepare_driver(self._driver)

Expand Down
10 changes: 9 additions & 1 deletion sqlspec/adapters/adbc/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,15 @@
_TYPE_COERCION_DISPATCHERS: "dict[tuple[tuple[type, Callable[[Any], Any]], ...], TypeDispatcher[Callable[[Any], Any]]]" = {}


def detect_dialect(connection: Any, logger: Any | None = None) -> str:
def detect_dialect(connection: Any, logger: Any | None = None, *, fallback_dialect: "str | None" = None) -> str:
"""Detect database dialect from ADBC driver information.

Args:
connection: ADBC connection with driver metadata.
logger: Optional logger for diagnostics.
fallback_dialect: Pre-resolved dialect from config (e.g. from
``resolve_dialect_from_config``). Used when introspection
yields no match, before defaulting to ``postgres``.

Returns:
Detected dialect name, defaulting to ``postgres``.
Expand All @@ -182,6 +185,11 @@ def detect_dialect(connection: Any, logger: Any | None = None) -> str:
if logger is not None:
logger.debug("Dialect detection failed: %s", exc)

if fallback_dialect:
if logger is not None:
logger.debug("Dialect from connection_config: %s (introspection yielded no match)", fallback_dialect)
return fallback_dialect

if logger is not None:
logger.warning("Could not determine dialect from driver info. Defaulting to 'postgres'.")
return "postgres"
Expand Down
14 changes: 12 additions & 2 deletions sqlspec/adapters/adbc/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,10 @@ def __init__(
connection: "AdbcConnection",
statement_config: "StatementConfig | None" = None,
driver_features: "dict[str, Any] | None" = None,
*,
dialect: "str | None" = None,
) -> None:
self._detected_dialect = detect_dialect(connection, logger)
self._detected_dialect = detect_dialect(connection, logger, fallback_dialect=dialect)

if statement_config is None:
base_config = get_statement_config(self._detected_dialect)
Expand Down Expand Up @@ -256,7 +258,15 @@ def dispatch_execute_script(self, cursor: "AdbcRawCursor", statement: "SQL") ->
# ─────────────────────────────────────────────────────────────────────────────

def begin(self) -> None:
"""Begin database transaction."""
"""Begin database transaction.

ADBC SQLite holds an implicit transaction after the first DML and
rejects an explicit ``BEGIN`` with "cannot start a transaction
within a transaction." On that dialect this is a no-op; the
implicit transaction is closed by ``commit()`` / ``rollback()``.
"""
if self._dialect_name == "sqlite":
return
try:
with self.with_cursor(self.connection) as cursor:
cursor.execute("BEGIN")
Expand Down
4 changes: 4 additions & 0 deletions sqlspec/adapters/bigquery/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,10 @@ def _setup_default_job_config(self) -> None:
if query_timeout_ms is not None:
job_config.job_timeout_ms = query_timeout_ms

job_timeout_ms = self.connection_config.get("job_timeout_ms")
if job_timeout_ms is not None:
job_config.job_timeout_ms = job_timeout_ms

self.connection_config["default_query_job_config"] = job_config

def create_connection(self) -> BigQueryConnection:
Expand Down
27 changes: 1 addition & 26 deletions sqlspec/adapters/bigquery/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import datetime
import importlib
import io
import os
from decimal import Decimal
from typing import TYPE_CHECKING, Any, cast

Expand Down Expand Up @@ -57,7 +56,6 @@
"create_mapped_exception",
"create_parameters",
"default_statement_config",
"detect_emulator",
"driver_profile",
"extract_insert_table",
"is_simple_insert",
Expand Down Expand Up @@ -335,27 +333,6 @@ def _inline_bigquery_literals(
return str(transformed_expression.sql(dialect="bigquery"))


def detect_emulator(connection: "BigQueryConnection") -> bool:
"""Detect whether the BigQuery client targets an emulator endpoint."""
emulator_host = os.getenv("BIGQUERY_EMULATOR_HOST") or os.getenv("BIGQUERY_EMULATOR_HOST_HTTP")
if emulator_host:
return True

try:
inner_connection = cast("Any", connection)._connection
except AttributeError:
inner_connection = None
if inner_connection is None:
return False
try:
api_base_url = inner_connection.API_BASE_URL
except AttributeError:
api_base_url = ""
if not api_base_url:
return False
return "googleapis.com" not in api_base_url


def _should_retry_bigquery_job(exception: Exception) -> bool:
"""Return True when a BigQuery job exception is safe to retry."""
if not isinstance(exception, GoogleCloudError):
Expand All @@ -381,10 +358,8 @@ def _should_retry_bigquery_job(exception: Exception) -> bool:
return False


def build_retry(deadline: float, using_emulator: bool) -> "Retry | None":
def build_retry(deadline: float) -> "Retry":
"""Build retry policy for job restarts based on error reason codes."""
if using_emulator:
return None
return Retry(predicate=_should_retry_bigquery_job, deadline=deadline)


Expand Down
5 changes: 1 addition & 4 deletions sqlspec/adapters/bigquery/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
collect_rows,
create_mapped_exception,
default_statement_config,
detect_emulator,
driver_profile,
is_simple_insert,
normalize_script_rowcount,
Expand Down Expand Up @@ -103,7 +102,6 @@ class BigQueryDriver(SyncDriverAdapterBase):
"_json_serializer",
"_literal_inliner",
"_type_converter",
"_using_emulator",
)
dialect = "bigquery"

Expand Down Expand Up @@ -132,9 +130,8 @@ def __init__(
self._default_query_job_config: QueryJobConfig | None = (driver_features or {}).get("default_query_job_config")
self._data_dictionary: BigQueryDataDictionary | None = None
self._column_name_cache: dict[int, tuple[Any, list[str]]] = {}
self._using_emulator = detect_emulator(connection)
self._job_retry_deadline = float(features.get("job_retry_deadline", 60.0))
self._job_retry = build_retry(self._job_retry_deadline, self._using_emulator)
self._job_retry = build_retry(self._job_retry_deadline)

# ─────────────────────────────────────────────────────────────────────────────
# CORE DISPATCH METHODS
Expand Down
48 changes: 34 additions & 14 deletions sqlspec/adapters/spanner/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@

__all__ = ("SpannerConnectionParams", "SpannerDriverFeatures", "SpannerPoolParams", "SpannerSyncConfig")

_DEFAULT_SESSION_TRANSACTION: bool = True
"""Default ``transaction`` flag for ``provide_session`` / ``provide_connection``.

``True`` yields a write-capable :class:`Transaction` context matching every
other sqlspec adapter. Read-only :class:`Snapshot` contexts are available via
:meth:`SpannerSyncConfig.provide_read_session`. Pulled into a module-level
constant so an eventual ``SpannerAsyncConfig`` can import the same default."""


class SpannerConnectionParams(TypedDict):
"""Spanner connection parameters."""
Expand Down Expand Up @@ -278,27 +286,38 @@ def _close_pool(self) -> None:
self._client = None
self._database = None

def provide_connection(self, *args: Any, transaction: "bool" = False, **kwargs: Any) -> "SpannerConnectionContext":
"""Yield a Snapshot (default) or Transaction context from the configured pool.
def provide_connection(
self, *args: Any, transaction: "bool" = _DEFAULT_SESSION_TRANSACTION, **kwargs: Any
) -> "SpannerConnectionContext":
"""Yield a Transaction (default) or Snapshot context from the configured pool.

Args:
*args: Additional positional arguments (unused, for interface compatibility).
transaction: If True, yields a Transaction context that supports
execute_update() for DML statements. If False (default), yields
transaction: If True (default), yields a Transaction context that
supports execute_update() for DML statements. If False, yields
a read-only Snapshot context for SELECT queries.
**kwargs: Additional keyword arguments (unused, for interface compatibility).
"""
return SpannerConnectionContext(self, transaction=transaction)

def provide_session(
self, *args: Any, statement_config: "StatementConfig | None" = None, transaction: "bool" = False, **kwargs: Any
self,
*args: Any,
statement_config: "StatementConfig | None" = None,
transaction: "bool" = _DEFAULT_SESSION_TRANSACTION,
**kwargs: Any,
) -> "SpannerSessionContext":
"""Provide a Spanner driver session context manager.

Returns a write-capable Transaction session by default, matching every
other sqlspec adapter. Pass ``transaction=False`` or use
:meth:`provide_read_session` to obtain a read-only Snapshot session.

Args:
*args: Additional arguments.
statement_config: Optional statement configuration override.
transaction: Whether to use a transaction.
transaction: Whether to use a Transaction (True, default) or
Snapshot (False).
**kwargs: Additional keyword arguments.

Returns:
Expand All @@ -318,17 +337,18 @@ def provide_session(
def provide_write_session(
self, *args: Any, statement_config: "StatementConfig | None" = None, **kwargs: Any
) -> "SpannerSessionContext":
"""Provide a Spanner driver write session context manager.
"""Provide a write-capable Spanner session (alias for :meth:`provide_session`)."""
return self.provide_session(*args, statement_config=statement_config, transaction=True, **kwargs)

Args:
*args: Additional arguments.
statement_config: Optional statement configuration override.
**kwargs: Additional keyword arguments.
def provide_read_session(
self, *args: Any, statement_config: "StatementConfig | None" = None, **kwargs: Any
) -> "SpannerSessionContext":
"""Provide a read-only Snapshot Spanner session.

Returns:
A Spanner driver write session context manager.
Use for query workloads that benefit from Spanner's snapshot reads.
For DDL/DML, use :meth:`provide_session` (write-capable by default).
"""
return self.provide_session(*args, statement_config=statement_config, transaction=True, **kwargs)
return self.provide_session(*args, statement_config=statement_config, transaction=False, **kwargs)

def get_signature_namespace(self) -> "dict[str, Any]":
"""Get the signature namespace for SpannerSyncConfig types.
Expand Down
12 changes: 8 additions & 4 deletions sqlspec/adapters/spanner/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
from sqlspec.exceptions import SQLConversionError
from sqlspec.utils.serializers import from_json

_READ_ONLY_SNAPSHOT_ERROR_MESSAGE = (
"Cannot execute DML in a read-only Snapshot context. "
"SpannerSyncConfig.provide_session() opens a write-capable Transaction by default; "
"the current session must have been opened via SpannerSyncConfig.provide_read_session()."
)

if TYPE_CHECKING:
from collections.abc import Callable

Expand Down Expand Up @@ -160,8 +166,7 @@ def dispatch_execute(self, cursor: "SpannerConnection", statement: "SQL") -> Exe
row_count = writer.execute_update(sql, params=coerced_params, param_types=param_types_map)
return self.create_execution_result(cursor, rowcount_override=row_count)

msg = "Cannot execute DML in a read-only Snapshot context."
raise SQLConversionError(msg)
raise SQLConversionError(_READ_ONLY_SNAPSHOT_ERROR_MESSAGE)

def dispatch_execute_many(self, cursor: "SpannerConnection", statement: "SQL") -> ExecutionResult:
if not supports_batch_update(cursor):
Expand Down Expand Up @@ -210,8 +215,7 @@ def dispatch_execute_script(self, cursor: "SpannerConnection", statement: "SQL")
is_select = stmt.upper().strip().startswith("SELECT")
coerced_params = self._coerce_params(script_params)
if not is_select and not is_transaction:
msg = "Cannot execute DML in a read-only Snapshot context."
raise SQLConversionError(msg)
raise SQLConversionError(_READ_ONLY_SNAPSHOT_ERROR_MESSAGE)
if not is_select and is_transaction:
writer = cast("_SpannerWriteProtocol", cursor)
writer.execute_update(stmt, params=coerced_params, param_types=self._infer_param_types(coerced_params))
Expand Down
4 changes: 2 additions & 2 deletions sqlspec/data_dictionary/sql/postgres/indexes.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ SELECT
t.relname as table_name,
ix.indisunique as is_unique,
ix.indisprimary as is_primary,
array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum)) as columns
array_agg(a.attname::text ORDER BY array_position(ix.indkey, a.attnum)) as columns
FROM
pg_class t,
pg_class i,
Expand Down Expand Up @@ -34,7 +34,7 @@ SELECT
t.relname as table_name,
ix.indisunique as is_unique,
ix.indisprimary as is_primary,
array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum)) as columns
array_agg(a.attname::text ORDER BY array_position(ix.indkey, a.attnum)) as columns
FROM
pg_class t,
pg_class i,
Expand Down
Loading
Loading