Skip to content
Merged
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
8 changes: 4 additions & 4 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Database Event Channels
- Introduced the ``events`` extension migrations (``ext_events_0001``) which create the durable queue table plus composite index.
- Added the first native backend (AsyncPG LISTEN/NOTIFY) enabled via ``driver_features["events_backend"] = "listen_notify"``; the API automatically falls back to the queue backend for other adapters.
- Introduced experimental Oracle Advanced Queuing support (sync adapters) via ``driver_features["events_backend"] = "advanced_queue"`` with automatic fallback when AQ is unavailable.
- Documented configuration patterns (queue table naming, lease/retention windows, Oracle ``INMEMORY`` toggle, Postgres native mode) in :doc:`/guides/events/database-event-channels`.
- Documented configuration patterns (queue table naming, lease/retention windows, Oracle ``INMEMORY`` toggle, Postgres native mode) for database event channels.
- Event telemetry now tracks ``events.publish``, ``events.publish.native``, ``events.deliver``, ``events.ack``, ``events.nack``, ``events.shutdown`` and listener lifecycle, so Prometheus/Otel exporters see event workloads alongside query metrics.
- Added adapter-specific runtime hints (asyncmy, duckdb, bigquery/adbc) plus a ``poll_interval`` extension option so operators can tune leases and cadence per database.
- Publishing, dequeue, ack, nack, and shutdown operations now emit ``sqlspec.events.*`` spans whenever ``extension_config["otel"]`` is enabled, giving full trace coverage without extra plumbing.
Expand Down Expand Up @@ -144,7 +144,7 @@ Simple search and replace in your codebase:
- Reduces cognitive load when switching between adapters
- Clearer API for new users

**See:** :doc:`/guides/migration/connection-config` for detailed migration guide with before/after examples for all adapters.
**See** the connection configuration section in :doc:`usage/configuration` for detailed migration guidance with before/after examples for all adapters.

Query Stack Documentation Suite
--------------------------------
Expand Down Expand Up @@ -308,7 +308,7 @@ Example conversion:
**Documentation:**

- Complete CLI reference: :doc:`usage/cli`
- Workflow guide: :ref:`hybrid-versioning-guide`
- Workflow guide for hybrid versioning
- CI integration examples for GitHub Actions and GitLab CI

Shell Completion Support
Expand Down Expand Up @@ -377,7 +377,7 @@ Extension migrations now receive automatic version prefixes and configuration ha
"include_extensions": ["adk"] # Simple string list
}

**Configuration Guide**: See :doc:`/migration_guides/extension_config`
**Configuration Guide**: See :doc:`usage/migrations` for extension configuration details.

Features
--------
Expand Down
4 changes: 0 additions & 4 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@
"sphinxcontrib.mermaid",
"numpydoc",
"sphinx_iconify",
"sphinx_docsearch",
"sphinx_datatables",
"jupyter_sphinx",
"nbsphinx",
Expand Down Expand Up @@ -167,9 +166,6 @@
# https://sphinx-copybutton.readthedocs.io/en/latest/use.html#strip-and-configure-input-prompts-for-code-cells
copybutton_prompt_text = "$ "

docsearch_app_id = os.getenv("DOCSEARCH_APP_ID", "disabled")
docsearch_api_key = os.getenv("DOCSEARCH_SEARCH_API_KEY", "disabled")
docsearch_index_name = os.getenv("DOCSEARCH_INDEX_NAME", "disabled")
nbsphinx_requirejs_path = ""
jupyter_sphinx_require_url = ""

Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ doc = [
"sphinx-autodoc-typehints",
"numpydoc",
"sphinx-iconify",
"sphinx-docsearch",
"jupyter-sphinx",
"nbsphinx",
]
Expand Down
4 changes: 2 additions & 2 deletions sqlspec/adapters/adbc/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"resolve_rowcount",
)

_COLUMN_NAME_CACHE_MAX_SIZE: int = 256
COLUMN_CACHE_MAX_SIZE: int = 256

DIALECT_PATTERNS: "dict[str, tuple[str, ...]]" = {
"postgres": ("postgres", "postgresql"),
Expand Down Expand Up @@ -722,7 +722,7 @@ def resolve_column_names(description: "list[Any] | None", cache: "dict[int, tupl
return cached[1]

column_names = [col[0] for col in description]
if len(cache) >= _COLUMN_NAME_CACHE_MAX_SIZE:
if len(cache) >= COLUMN_CACHE_MAX_SIZE:
cache.pop(next(iter(cache)))
cache[cache_key] = (description, column_names)
return column_names
Expand Down
4 changes: 2 additions & 2 deletions sqlspec/adapters/bigquery/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
HTTP_BAD_REQUEST = 400
HTTP_FORBIDDEN = 403
HTTP_SERVER_ERROR = 500
_COLUMN_NAME_CACHE_MAX_SIZE = 256
COLUMN_CACHE_MAX_SIZE = 256


def _identity(value: Any) -> Any:
Expand Down Expand Up @@ -546,7 +546,7 @@ def resolve_column_names(schema: Any | None, cache: "dict[int, tuple[Any, list[s
return cached[1]

column_names = [field.name for field in schema]
if len(cache) >= _COLUMN_NAME_CACHE_MAX_SIZE:
if len(cache) >= COLUMN_CACHE_MAX_SIZE:
cache.pop(next(iter(cache)))
cache[cache_key] = (schema, column_names)
return column_names
Expand Down
12 changes: 6 additions & 6 deletions sqlspec/adapters/oracledb/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@
_VERSION_COMPONENTS: int = 3
TYPE_CONVERTER = OracleOutputConverter()
_LOB_TYPE_NAME_MARKERS: "tuple[str, ...]" = ("LOB", "BFILE")
_FAST_SCALAR_TYPES: "tuple[type[Any], ...]" = (bool, int, float, str, bytes, bytearray, type(None))
_ROW_METADATA_CACHE_MAX_SIZE: int = 256
_SCALAR_PASSTHROUGH_TYPES: "tuple[type[Any], ...]" = (bool, int, float, str, bytes, bytearray, type(None))
ROW_CACHE_MAX_SIZE: int = 256

# Oracle ORA error code ranges for category detection
ORA_CHECK_CONSTRAINT = 2290
Expand Down Expand Up @@ -415,7 +415,7 @@ def resolve_row_metadata(
normalized_column_names = normalize_column_names(column_names, driver_features)
requires_lob_coercion = _description_requires_lob_coercion(description)

if len(cache) >= _ROW_METADATA_CACHE_MAX_SIZE:
if len(cache) >= ROW_CACHE_MAX_SIZE:
cache.pop(next(iter(cache)))
cache[cache_key] = (description, normalized_column_names, requires_lob_coercion)
return normalized_column_names, requires_lob_coercion
Expand All @@ -425,7 +425,7 @@ def _row_requires_lob_coercion(row: "tuple[Any, ...]") -> bool:
"""Return True when a row contains readable values that need LOB coercion."""
for value in row:
value_type = type(value)
if value_type in _FAST_SCALAR_TYPES:
if value_type in _SCALAR_PASSTHROUGH_TYPES:
continue
if is_readable(value):
return True
Expand All @@ -448,7 +448,7 @@ def _coerce_sync_row_values(row: "tuple[Any, ...]") -> "tuple[Any, ...]":
coerced_values: list[Any] | None = None
for index, value in enumerate(row):
value_type = type(value)
if value_type in _FAST_SCALAR_TYPES:
if value_type in _SCALAR_PASSTHROUGH_TYPES:
if coerced_values is not None:
coerced_values.append(value)
continue
Expand Down Expand Up @@ -494,7 +494,7 @@ async def _coerce_async_row_values(row: "tuple[Any, ...]") -> "tuple[Any, ...]":
coerced_values: list[Any] | None = None
for index, value in enumerate(row):
value_type = type(value)
if value_type in _FAST_SCALAR_TYPES:
if value_type in _SCALAR_PASSTHROUGH_TYPES:
if coerced_values is not None:
coerced_values.append(value)
continue
Expand Down
6 changes: 3 additions & 3 deletions sqlspec/adapters/psycopg/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
)

logger = get_logger("sqlspec.adapters.psycopg")
_COLUMN_NAME_CACHE_MAX_SIZE = 256
COLUMN_CACHE_MAX_SIZE = 256


class PsycopgPipelineMixin:
Expand Down Expand Up @@ -566,7 +566,7 @@ def _resolve_column_names(self, description: Any) -> list[str]:

column_names = [col.name for col in description]

if len(self._column_name_cache) >= _COLUMN_NAME_CACHE_MAX_SIZE:
if len(self._column_name_cache) >= COLUMN_CACHE_MAX_SIZE:
self._column_name_cache.pop(next(iter(self._column_name_cache)))
self._column_name_cache[cache_key] = (description, column_names)
return column_names
Expand Down Expand Up @@ -1051,7 +1051,7 @@ def _resolve_column_names(self, description: Any) -> list[str]:

column_names = [col.name for col in description]

if len(self._column_name_cache) >= _COLUMN_NAME_CACHE_MAX_SIZE:
if len(self._column_name_cache) >= COLUMN_CACHE_MAX_SIZE:
self._column_name_cache.pop(next(iter(self._column_name_cache)))
self._column_name_cache[cache_key] = (description, column_names)
return column_names
Expand Down
4 changes: 2 additions & 2 deletions sqlspec/adapters/spanner/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"supports_write",
)

_COLUMN_NAME_CACHE_MAX_SIZE: int = 128
COLUMN_CACHE_MAX_SIZE: int = 128


def build_profile() -> "DriverParameterProfile":
Expand Down Expand Up @@ -136,7 +136,7 @@ def resolve_column_names(fields: "Sequence[Any] | None", cache: "dict[int, tuple
return cached[1]

column_names = [field.name for field in fields]
if len(cache) >= _COLUMN_NAME_CACHE_MAX_SIZE:
if len(cache) >= COLUMN_CACHE_MAX_SIZE:
cache.pop(next(iter(cache)))
cache[cache_key] = (fields, column_names)
return column_names
Expand Down
7 changes: 4 additions & 3 deletions sqlspec/adapters/sqlite/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,9 @@ def execute_many(
return DMLResult(operation, affected_rows)
return super().execute_many(statement, parameters, *filters, statement_config=statement_config, **kwargs)

def _qc_execute_direct(self, sql: str, params: "tuple[Any, ...] | list[Any]", cached: "CachedQuery") -> "SQLResult":
def _stmt_cache_execute_direct(
self, sql: str, params: "tuple[Any, ...] | list[Any]", cached: "CachedQuery"
) -> "SQLResult":
"""Execute cached query through SQLite connection.execute fast path.

This bypasses cursor context-manager overhead for repeated cached
Expand Down Expand Up @@ -269,7 +271,6 @@ def _qc_execute_direct(self, sql: str, params: "tuple[Any, ...] | list[Any]", ca
if column_names is None:
description = cursor.description
column_names = [col[0] for col in description] if description else []
cached.column_names = column_names
execution_result = self.create_execution_result(
cursor,
selected_data=fetched_data,
Expand All @@ -278,7 +279,7 @@ def _qc_execute_direct(self, sql: str, params: "tuple[Any, ...] | list[Any]", ca
is_select_result=True,
row_format="tuple",
)
direct_statement = self._qc_build_direct(
direct_statement = self._stmt_cache_build_direct(
sql, params, cached, params, params_are_simple=True, compiled_sql=cached.compiled_sql
)
return self.build_statement_result(direct_statement, execution_result)
Expand Down
32 changes: 14 additions & 18 deletions sqlspec/core/result/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"""

from abc import ABC, abstractmethod
from collections.abc import Iterable, Iterator
from collections.abc import Iterable, Iterator, Sequence
from typing import TYPE_CHECKING, Any, cast, overload

from mypy_extensions import mypyc_attr
Expand Down Expand Up @@ -48,13 +48,9 @@

T = TypeVar("T")
_EMPTY_RESULT_STATEMENT = SQL("-- empty stack result --")
_EMPTY_RESULT_DATA: list[Any] = []
_EMPTY_DML_METADATA: dict[str, Any] = {}
_EMPTY_DML_COLUMN_NAMES: list[str] = []
_EMPTY_DML_INSERTED_IDS: list[int | str] = []
_EMPTY_DML_STATEMENT_RESULTS: list["SQLResult"] = []
_EMPTY_DML_ERRORS: list[str] = []
_TWO_COLUMNS_FASTPATH = 2
_EMPTY_RESULT_DATA: "tuple[()]" = ()
_DEFAULT_DML_METADATA: dict[str, Any] = {}
_TWO_COLUMN_THRESHOLD = 2


@mypyc_attr(allow_interpreted_subclasses=False)
Expand Down Expand Up @@ -186,7 +182,7 @@ class SQLResult(StatementResult):
def __init__(
self,
statement: "SQL",
data: "list[Any] | None" = None,
data: "Sequence[Any] | None" = None,
rows_affected: int = 0,
last_inserted_id: int | str | None = None,
execution_time: float | None = None,
Expand Down Expand Up @@ -294,7 +290,7 @@ def _get_rows(self) -> "list[dict[str, Any]]":
elif len(col_names) == 1:
key = col_names[0]
self._materialized_dicts = [{key: row[0]} for row in raw]
elif len(col_names) == _TWO_COLUMNS_FASTPATH:
elif len(col_names) == _TWO_COLUMN_THRESHOLD:
key0, key1 = col_names
self._materialized_dicts = [{key0: row[0], key1: row[1]} for row in raw]
else:
Expand Down Expand Up @@ -980,7 +976,7 @@ def is_success(self) -> bool:
return True

def get_data(self) -> "list[Any]":
return _EMPTY_RESULT_DATA
return []


@mypyc_attr(allow_interpreted_subclasses=False)
Expand All @@ -1001,7 +997,7 @@ def __init__(self, op_type: "OperationType", rows_affected: int = 0) -> None:
self.rows_affected = rows_affected
self.last_inserted_id = None
self.execution_time = None
self.metadata = _EMPTY_DML_METADATA
self.metadata = _DEFAULT_DML_METADATA

self.error = None
self._operation_type = op_type
Expand All @@ -1010,12 +1006,12 @@ def __init__(self, op_type: "OperationType", rows_affected: int = 0) -> None:
self._row_format = "dict"
self._materialized_dicts = None

self.column_names = _EMPTY_DML_COLUMN_NAMES
self.column_names: list[str] = []
self.total_count = 0
self.has_more = False
self.inserted_ids = _EMPTY_DML_INSERTED_IDS
self.statement_results = _EMPTY_DML_STATEMENT_RESULTS
self.errors = _EMPTY_DML_ERRORS
self.inserted_ids: list[int | str] = []
self.statement_results: list[SQLResult] = []
self.errors: list[str] = []
self.total_statements = 0
self.successful_statements = 0

Expand All @@ -1026,11 +1022,11 @@ def is_success(self) -> bool:
return self.rows_affected >= 0

def get_data(self, *, schema_type: "type[SchemaT] | None" = None) -> "list[Any]":
return _EMPTY_RESULT_DATA
return []

def set_metadata(self, key: str, value: Any) -> None:
# Copy-on-write to preserve low-allocation defaults for hot DML paths.
if self.metadata is _EMPTY_DML_METADATA:
if self.metadata is _DEFAULT_DML_METADATA:
self.metadata = {key: value}
return
self.metadata[key] = value
Expand Down
10 changes: 5 additions & 5 deletions sqlspec/core/statement.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,14 +192,14 @@ class SQL:
"_dialect",
"_filters",
"_hash",
"_is_cache_direct",
"_is_many",
"_is_script",
"_named_parameters",
"_original_parameters",
"_pooled",
"_positional_parameters",
"_processed_state",
"_qc_is_direct",
"_raw_expression",
"_raw_sql",
"_sql_param_counters",
Expand All @@ -210,7 +210,7 @@ class SQL:
_sql_param_counters: "dict[str, int]"

@classmethod
def _qc_create_direct_sql(cls, sql: str, config: "StatementConfig", processed_state: "ProcessedState") -> "SQL":
def _create_cached_direct(cls, sql: str, config: "StatementConfig", processed_state: "ProcessedState") -> "SQL":
"""Create a minimal SQL object for direct (fast-path) execution.

Bypasses standard __init__ and parameter processing.
Expand All @@ -222,7 +222,7 @@ def _qc_create_direct_sql(cls, sql: str, config: "StatementConfig", processed_st
stmt._dialect = stmt._normalize_dialect(config.dialect)
stmt._processed_state = processed_state
stmt._pooled = True
stmt._qc_is_direct = True
stmt._is_cache_direct = True
stmt._is_many = False
stmt._is_script = False
return stmt
Expand All @@ -249,7 +249,7 @@ def __init__(
self._dialect = self._normalize_dialect(config.dialect)
self._compiled_from_cache = False
self._pooled = False
self._qc_is_direct = False
self._is_cache_direct = False
self._processed_state: EmptyEnum | ProcessedState = Empty
self._hash: int | None = None
self._filters: list[StatementFilter] = []
Expand Down Expand Up @@ -296,7 +296,7 @@ def reset(self) -> None:
if self._pooled and not self._compiled_from_cache and self._processed_state is not Empty:
get_processed_state_pool().release(self._processed_state)
self._compiled_from_cache = False
self._qc_is_direct = False
self._is_cache_direct = False
self._processed_state = Empty
self._hash = None
self._filters.clear()
Expand Down
Loading