From 4adb92ed0809b734663b6e109df133da1e15e98b Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sat, 21 Feb 2026 02:47:09 +0000 Subject: [PATCH 1/4] fix(docs): restore search by removing unused sphinx-docsearch sphinx_docsearch disabled Sphinx's built-in search but Algolia credentials were never configured, leaving the deployed docs with no search functionality. Remove the extension and its dependency to restore Shibuya's built-in client-side search. Also fix 4 broken cross-references in changelog.rst that pointed to non-existent guide directories. --- docs/changelog.rst | 8 ++++---- docs/conf.py | 4 ---- pyproject.toml | 1 - uv.lock | 17 ----------------- 4 files changed, 4 insertions(+), 26 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 16ca3bcd..c737cf93 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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. @@ -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 -------------------------------- @@ -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 @@ -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 -------- diff --git a/docs/conf.py b/docs/conf.py index 2c766b03..86f52e86 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -77,7 +77,6 @@ "sphinxcontrib.mermaid", "numpydoc", "sphinx_iconify", - "sphinx_docsearch", "sphinx_datatables", "jupyter_sphinx", "nbsphinx", @@ -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 = "" diff --git a/pyproject.toml b/pyproject.toml index 8b8789a4..6738ada6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,7 +99,6 @@ doc = [ "sphinx-autodoc-typehints", "numpydoc", "sphinx-iconify", - "sphinx-docsearch", "jupyter-sphinx", "nbsphinx", ] diff --git a/uv.lock b/uv.lock index 0736ce56..29bbad62 100644 --- a/uv.lock +++ b/uv.lock @@ -6621,19 +6621,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/cf/45dd359f6ca0c3762ce0490f681da242f0530c49c81050c035c016bfdd3a/sphinx_design-0.7.0-py3-none-any.whl", hash = "sha256:f82bf179951d58f55dca78ab3706aeafa496b741a91b1911d371441127d64282", size = 2220350, upload-time = "2026-01-19T13:12:51.077Z" }, ] -[[package]] -name = "sphinx-docsearch" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a5/05/5ac17452f431536ca7810745e6368232744ea24f7b255e13846e085d69a5/sphinx_docsearch-0.3.0.tar.gz", hash = "sha256:24e131a6cd1c7f3bb28080576a31f767c7bb8d84a9dc947de1fa1ed7aaf69e70", size = 246351, upload-time = "2026-01-20T08:26:14.646Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/c5/5914fb7e854180bde310117c227e986b1491933c3061bd063594c51d5d17/sphinx_docsearch-0.3.0-py3-none-any.whl", hash = "sha256:fa7abbdfcf5f65c529bb49be0e663fb4687982fa0f32d614e2ad1687f6a65d23", size = 141598, upload-time = "2026-01-20T08:26:13.121Z" }, -] - [[package]] name = "sphinx-iconify" version = "0.3.0" @@ -7218,7 +7205,6 @@ dev = [ { name = "sphinx-datatables" }, { name = "sphinx-design", version = "0.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx-design", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "sphinx-docsearch" }, { name = "sphinx-iconify" }, { name = "sphinx-paramlinks" }, { name = "sphinx-tabs" }, @@ -7255,7 +7241,6 @@ doc = [ { name = "sphinx-datatables" }, { name = "sphinx-design", version = "0.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx-design", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "sphinx-docsearch" }, { name = "sphinx-iconify" }, { name = "sphinx-paramlinks" }, { name = "sphinx-tabs" }, @@ -7426,7 +7411,6 @@ dev = [ { name = "sphinx-copybutton", specifier = ">=0.5.2" }, { name = "sphinx-datatables" }, { name = "sphinx-design", specifier = ">=0.5.0" }, - { name = "sphinx-docsearch" }, { name = "sphinx-iconify" }, { name = "sphinx-paramlinks", specifier = ">=0.6.0" }, { name = "sphinx-tabs" }, @@ -7458,7 +7442,6 @@ doc = [ { name = "sphinx-copybutton", specifier = ">=0.5.2" }, { name = "sphinx-datatables" }, { name = "sphinx-design", specifier = ">=0.5.0" }, - { name = "sphinx-docsearch" }, { name = "sphinx-iconify" }, { name = "sphinx-paramlinks", specifier = ">=0.6.0" }, { name = "sphinx-tabs" }, From 1b378d00339306f4e31776369796c466c2ecb3e0 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sat, 21 Feb 2026 22:29:50 +0000 Subject: [PATCH 2/4] fix(security): harden hot-path optimizations from PR #368 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Convert shared mutable _EMPTY_DATA and _EMPTY_RESULT_DATA sentinels to immutable tuples, preventing global data leakage if consumers mutate result.data - Remove unsafe write-back of column_names to shared CachedQuery in SQLite driver, eliminating TOCTOU race in multi-threaded usage - Narrow exception catch in sync direct fast path from (AttributeError, NotImplementedError, TypeError) to (AttributeError, NotImplementedError) — TypeError from cursor.execute() indicates real parameter mismatches - Remove _FAST_NO_COERCION_TYPES short-circuit that bypassed user-registered parent-class coercions for primitive types --- sqlspec/adapters/sqlite/driver.py | 1 - sqlspec/core/result/_base.py | 18 +++++++++--------- sqlspec/driver/_common.py | 6 +----- sqlspec/driver/_sync.py | 2 +- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/sqlspec/adapters/sqlite/driver.py b/sqlspec/adapters/sqlite/driver.py index b394792f..c03e218b 100644 --- a/sqlspec/adapters/sqlite/driver.py +++ b/sqlspec/adapters/sqlite/driver.py @@ -269,7 +269,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, diff --git a/sqlspec/core/result/_base.py b/sqlspec/core/result/_base.py index 835f9a6d..8684265e 100644 --- a/sqlspec/core/result/_base.py +++ b/sqlspec/core/result/_base.py @@ -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 @@ -48,12 +48,12 @@ T = TypeVar("T") _EMPTY_RESULT_STATEMENT = SQL("-- empty stack result --") -_EMPTY_RESULT_DATA: list[Any] = [] +_EMPTY_RESULT_DATA: "tuple[()]" = () _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] = [] +_EMPTY_DML_COLUMN_NAMES: "tuple[()]" = () +_EMPTY_DML_INSERTED_IDS: "tuple[()]" = () +_EMPTY_DML_STATEMENT_RESULTS: "tuple[()]" = () +_EMPTY_DML_ERRORS: "tuple[()]" = () _TWO_COLUMNS_FASTPATH = 2 @@ -186,7 +186,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, @@ -980,7 +980,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) @@ -1026,7 +1026,7 @@ 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. diff --git a/sqlspec/driver/_common.py b/sqlspec/driver/_common.py index 82e9f6f3..fd4910ec 100644 --- a/sqlspec/driver/_common.py +++ b/sqlspec/driver/_common.py @@ -800,8 +800,7 @@ class ExecutionResult(NamedTuple): _DEFAULT_METADATA: Final = {"status_message": "OK"} -_EMPTY_DATA: Final["list[Any]"] = [] -_FAST_NO_COERCION_TYPES: Final = (str, int, float, bytes, bytearray, memoryview, type(None)) +_EMPTY_DATA: Final[tuple[()]] = () @mypyc_attr(allow_interpreted_subclasses=True) @@ -1610,9 +1609,6 @@ def _apply_coercion(self, value: object, type_coercion_map: "dict[type, Callable if exact_converter is not None: return exact_converter(unwrapped_value) - if value_type in _FAST_NO_COERCION_TYPES: - return unwrapped_value - for type_check, converter in type_coercion_map.items(): if type_check is value_type: continue diff --git a/sqlspec/driver/_sync.py b/sqlspec/driver/_sync.py index db98655f..387e3a22 100644 --- a/sqlspec/driver/_sync.py +++ b/sqlspec/driver/_sync.py @@ -391,7 +391,7 @@ def _qc_execute_direct(self, sql: str, params: "tuple[Any, ...] | list[Any]", ca affected_rows = self.resolve_rowcount(cursor) return DMLResult(cached.operation_type, affected_rows) - except (AttributeError, NotImplementedError, TypeError): + except (AttributeError, NotImplementedError): # Cursor is not DB-API compatible for direct execution. # Fall back to adapter dispatch path. pass From 8aeebfe8606411359579b68a90c0f15e6603e870 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sun, 22 Feb 2026 14:54:28 +0000 Subject: [PATCH 3/4] refactor: rename hot-path identifiers for readability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace cryptic abbreviations with self-documenting names: - _qc_* → _stmt_cache_* (query cache methods/attributes) - _ps_pool → _processed_state_pool, _sql_pool → _statement_pool - QC_MAX_SIZE → STMT_CACHE_MAX_SIZE - _FAST_SCALAR_TYPES → _SCALAR_PASSTHROUGH_TYPES - _COLUMN_NAME_CACHE_MAX_SIZE → COLUMN_CACHE_MAX_SIZE - _ROW_METADATA_CACHE_MAX_SIZE → ROW_CACHE_MAX_SIZE - _TWO_COLUMNS_FASTPATH → _TWO_COLUMN_THRESHOLD - _EMPTY_DATA → _EMPTY_DML_DATA, _DEFAULT_METADATA → _DEFAULT_DML_METADATA --- sqlspec/adapters/adbc/core.py | 4 +- sqlspec/adapters/bigquery/core.py | 4 +- sqlspec/adapters/oracledb/core.py | 12 +-- sqlspec/adapters/psycopg/driver.py | 6 +- sqlspec/adapters/spanner/core.py | 4 +- sqlspec/adapters/sqlite/driver.py | 6 +- sqlspec/core/result/_base.py | 10 +-- sqlspec/core/statement.py | 10 +-- sqlspec/driver/_async.py | 18 ++-- sqlspec/driver/_common.py | 104 +++++++++++----------- sqlspec/driver/_query_cache.py | 6 +- sqlspec/driver/_sync.py | 16 ++-- tests/unit/adapters/test_sync_adapters.py | 6 +- tests/unit/driver/test_query_cache.py | 54 +++++------ tools/scripts/analyze_profile.py | 10 +-- tools/scripts/bench_subsystems.py | 28 +++--- 16 files changed, 151 insertions(+), 147 deletions(-) diff --git a/sqlspec/adapters/adbc/core.py b/sqlspec/adapters/adbc/core.py index 036d0d93..ccaff5e0 100644 --- a/sqlspec/adapters/adbc/core.py +++ b/sqlspec/adapters/adbc/core.py @@ -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"), @@ -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 diff --git a/sqlspec/adapters/bigquery/core.py b/sqlspec/adapters/bigquery/core.py index d69a59d1..10d563de 100644 --- a/sqlspec/adapters/bigquery/core.py +++ b/sqlspec/adapters/bigquery/core.py @@ -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: @@ -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 diff --git a/sqlspec/adapters/oracledb/core.py b/sqlspec/adapters/oracledb/core.py index 2470d66a..c21f2fa6 100644 --- a/sqlspec/adapters/oracledb/core.py +++ b/sqlspec/adapters/oracledb/core.py @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/sqlspec/adapters/psycopg/driver.py b/sqlspec/adapters/psycopg/driver.py index 2565c6bd..59b1bfa5 100644 --- a/sqlspec/adapters/psycopg/driver.py +++ b/sqlspec/adapters/psycopg/driver.py @@ -72,7 +72,7 @@ ) logger = get_logger("sqlspec.adapters.psycopg") -_COLUMN_NAME_CACHE_MAX_SIZE = 256 +COLUMN_CACHE_MAX_SIZE = 256 class PsycopgPipelineMixin: @@ -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 @@ -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 diff --git a/sqlspec/adapters/spanner/core.py b/sqlspec/adapters/spanner/core.py index 555a84c5..5aa540ca 100644 --- a/sqlspec/adapters/spanner/core.py +++ b/sqlspec/adapters/spanner/core.py @@ -41,7 +41,7 @@ "supports_write", ) -_COLUMN_NAME_CACHE_MAX_SIZE: int = 128 +COLUMN_CACHE_MAX_SIZE: int = 128 def build_profile() -> "DriverParameterProfile": @@ -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 diff --git a/sqlspec/adapters/sqlite/driver.py b/sqlspec/adapters/sqlite/driver.py index c03e218b..13c76fd1 100644 --- a/sqlspec/adapters/sqlite/driver.py +++ b/sqlspec/adapters/sqlite/driver.py @@ -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 @@ -277,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) diff --git a/sqlspec/core/result/_base.py b/sqlspec/core/result/_base.py index 8684265e..5743b420 100644 --- a/sqlspec/core/result/_base.py +++ b/sqlspec/core/result/_base.py @@ -49,12 +49,12 @@ T = TypeVar("T") _EMPTY_RESULT_STATEMENT = SQL("-- empty stack result --") _EMPTY_RESULT_DATA: "tuple[()]" = () -_EMPTY_DML_METADATA: dict[str, Any] = {} +_DEFAULT_DML_METADATA: dict[str, Any] = {} _EMPTY_DML_COLUMN_NAMES: "tuple[()]" = () _EMPTY_DML_INSERTED_IDS: "tuple[()]" = () _EMPTY_DML_STATEMENT_RESULTS: "tuple[()]" = () _EMPTY_DML_ERRORS: "tuple[()]" = () -_TWO_COLUMNS_FASTPATH = 2 +_TWO_COLUMN_THRESHOLD = 2 @mypyc_attr(allow_interpreted_subclasses=False) @@ -294,7 +294,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: @@ -1001,7 +1001,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 @@ -1030,7 +1030,7 @@ def get_data(self, *, schema_type: "type[SchemaT] | None" = None) -> "list[Any]" 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 diff --git a/sqlspec/core/statement.py b/sqlspec/core/statement.py index 50929865..a99cd4ed 100644 --- a/sqlspec/core/statement.py +++ b/sqlspec/core/statement.py @@ -192,6 +192,7 @@ class SQL: "_dialect", "_filters", "_hash", + "_is_cache_direct", "_is_many", "_is_script", "_named_parameters", @@ -199,7 +200,6 @@ class SQL: "_pooled", "_positional_parameters", "_processed_state", - "_qc_is_direct", "_raw_expression", "_raw_sql", "_sql_param_counters", @@ -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. @@ -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 @@ -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] = [] @@ -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() diff --git a/sqlspec/driver/_async.py b/sqlspec/driver/_async.py index 94ad0d8a..e0285014 100644 --- a/sqlspec/driver/_async.py +++ b/sqlspec/driver/_async.py @@ -343,12 +343,12 @@ def resolve_rowcount(self, cursor: Any) -> int: msg = "Adapter must implement resolve_rowcount() for direct execution path" raise NotImplementedError(msg) - async def _qc_execute_direct( + async def _stmt_cache_execute_direct( self, sql: str, params: "tuple[Any, ...] | list[Any]", cached: CachedQuery ) -> "SQLResult": """Execute pre-compiled query via ultra-fast path (async). - Uses _qc_build_direct + dispatch_execute since async drivers can't + Uses _stmt_cache_build_direct + dispatch_execute since async drivers can't call cursor.execute directly. For DML operations, returns DMLResult to bypass full SQLResult construction. @@ -361,7 +361,7 @@ async def _qc_execute_direct( SQLResult or DMLResult. """ compiled_sql = cached.compiled_sql - direct_statement = self._qc_build_direct( + direct_statement = self._stmt_cache_build_direct( sql, params, cached, params, params_are_simple=True, compiled_sql=compiled_sql ) @@ -420,7 +420,7 @@ async def _qc_execute_direct( finally: self._release_pooled_statement(direct_statement) - async def _qc_lookup(self, statement: str, params: "tuple[Any, ...] | list[Any]") -> "SQLResult | None": + async def _stmt_cache_lookup(self, statement: str, params: "tuple[Any, ...] | list[Any]") -> "SQLResult | None": """Attempt fast-path execution for cached query (async). Args: @@ -430,15 +430,15 @@ async def _qc_lookup(self, statement: str, params: "tuple[Any, ...] | list[Any]" Returns: SQLResult if cache hit and execution succeeds, None otherwise. """ - result = super()._qc_lookup(statement, params) + result = super()._stmt_cache_lookup(statement, params) if result is None: return None return await cast("Awaitable[SQLResult | None]", result) - async def _qc_execute(self, statement: "SQL") -> "SQLResult": + async def _stmt_cache_execute(self, statement: "SQL") -> "SQLResult": """Execute pre-compiled query via fast path (async). - The statement is already compiled by _qc_prepare, so dispatch_execute + The statement is already compiled by _stmt_cache_prepare_direct, so dispatch_execute will hit the fast path in _get_compiled_statement (is_processed check). """ exc_handler = self.handle_database_exceptions() @@ -535,14 +535,14 @@ async def execute( ) -> "SQLResult": """Execute a statement with parameter handling.""" if ( - self._qc_enabled + self._stmt_cache_enabled and (statement_config is None or statement_config is self.statement_config) and isinstance(statement, str) and len(parameters) == 1 and isinstance(parameters[0], (tuple, list)) and not kwargs ): - fast_result = await self._qc_lookup(statement, parameters[0]) + fast_result = await self._stmt_cache_lookup(statement, parameters[0]) if fast_result is not None: return fast_result sql_statement = self.prepare_statement( diff --git a/sqlspec/driver/_common.py b/sqlspec/driver/_common.py index fd4910ec..62e87c71 100644 --- a/sqlspec/driver/_common.py +++ b/sqlspec/driver/_common.py @@ -31,7 +31,7 @@ from sqlspec.core.statement import ProcessedState from sqlspec.data_dictionary._loader import get_data_dictionary_loader from sqlspec.data_dictionary._registry import get_dialect_config -from sqlspec.driver._query_cache import QC_MAX_SIZE, CachedQuery, QueryCache +from sqlspec.driver._query_cache import STMT_CACHE_MAX_SIZE, CachedQuery, QueryCache from sqlspec.driver._storage_helpers import CAPABILITY_HINTS from sqlspec.exceptions import ImproperConfigurationError, NotFoundError, SQLFileNotFoundError, StorageCapabilityError from sqlspec.observability import ObservabilityRuntime, get_trace_context, resolve_db_system @@ -799,8 +799,8 @@ class ExecutionResult(NamedTuple): DEFAULT_EXECUTION_RESULT: Final["tuple[object | None, int | None, object | None]"] = (None, None, None) -_DEFAULT_METADATA: Final = {"status_message": "OK"} -_EMPTY_DATA: Final[tuple[()]] = () +_DEFAULT_DML_METADATA: Final = {"status_message": "OK"} +_EMPTY_DML_DATA: Final[tuple[()]] = () @mypyc_attr(allow_interpreted_subclasses=True) @@ -809,11 +809,11 @@ class CommonDriverAttributesMixin: __slots__ = ( "_observability", - "_ps_pool", - "_qc", - "_qc_enabled", - "_sql_pool", + "_processed_state_pool", "_statement_cache", + "_statement_pool", + "_stmt_cache", + "_stmt_cache_enabled", "connection", "driver_features", "statement_config", @@ -843,19 +843,19 @@ def __init__( self.driver_features = driver_features or {} self._observability = observability self._statement_cache: dict[str, SQL] = {} - self._qc = QueryCache(QC_MAX_SIZE) - self._qc_enabled = False - self._sql_pool = get_sql_pool() - self._ps_pool = get_processed_state_pool() - self._update_qc_flag() + self._stmt_cache = QueryCache(STMT_CACHE_MAX_SIZE) + self._stmt_cache_enabled = False + self._statement_pool = get_sql_pool() + self._processed_state_pool = get_processed_state_pool() + self._update_stmt_cache_flag() def attach_observability(self, runtime: "ObservabilityRuntime") -> None: """Attach or replace the observability runtime.""" self._observability = runtime - self._update_qc_flag() + self._update_stmt_cache_flag() - def _update_qc_flag(self) -> None: - self._qc_enabled = bool(not self.statement_config._has_transformers and self.observability.is_idle) # pyright: ignore[reportPrivateUsage] + def _update_stmt_cache_flag(self) -> None: + self._stmt_cache_enabled = bool(not self.statement_config._has_transformers and self.observability.is_idle) # pyright: ignore[reportPrivateUsage] @property def observability(self) -> "ObservabilityRuntime": @@ -931,9 +931,9 @@ def _raise_storage_not_implemented(self, capability: str) -> None: def _release_pooled_statement(self, statement: "SQL") -> None: if getattr(statement, "_pooled", False): - self._sql_pool.release(statement) + self._statement_pool.release(statement) - def qc_rebind(self, params: "tuple[Any, ...] | list[Any]", cached: "CachedQuery") -> "ConvertedParameters": + def stmt_cache_rebind(self, params: "tuple[Any, ...] | list[Any]", cached: "CachedQuery") -> "ConvertedParameters": """Rebind parameters for a cached query.""" config = self.statement_config.parameter_config if not cached.input_named_parameters and not cached.applied_wrap_types and not config.type_coercion_map: @@ -953,7 +953,7 @@ def qc_rebind(self, params: "tuple[Any, ...] | list[Any]", cached: "CachedQuery" apply_wrap_types=cached.applied_wrap_types, ) - def _qc_build_direct( + def _stmt_cache_build_direct( self, sql: str, _params: "tuple[Any, ...] | list[Any]", @@ -973,7 +973,7 @@ def _qc_build_direct( ) effective_compiled_sql = cached.compiled_sql if compiled_sql is None else compiled_sql cached_state = cached.processed_state - direct_state = self._ps_pool.acquire() + direct_state = self._processed_state_pool.acquire() ProcessedState.__init__( direct_state, compiled_sql=effective_compiled_sql, @@ -991,13 +991,13 @@ def _qc_build_direct( is_many=False, ) # Fast-path: directly set internal attributes to avoid constructor overhead. - return SQL._qc_create_direct_sql(sql, self.statement_config, direct_state) # pyright: ignore[reportPrivateUsage] + return SQL._create_cached_direct(sql, self.statement_config, direct_state) # pyright: ignore[reportPrivateUsage] - def _qc_prepare_direct(self, statement: str, params: "tuple[Any, ...] | list[Any]") -> "SQL | None": + def _stmt_cache_prepare_direct(self, statement: str, params: "tuple[Any, ...] | list[Any]") -> "SQL | None": """Prepare direct execution if cache hit. Only essential checks in the hot lookup path. All detailed eligibility - validation happens at store time in _qc_store(). + validation happens at store time in _stmt_cache_store(). Args: statement: Raw SQL string. @@ -1006,16 +1006,16 @@ def _qc_prepare_direct(self, statement: str, params: "tuple[Any, ...] | list[Any Returns: Prepared SQL object with processed state if cache hit, None otherwise. """ - if not self._qc_enabled: + if not self._stmt_cache_enabled: return None - cached = self._qc.get(statement) + cached = self._stmt_cache.get(statement) if cached is None or cached.param_count != len(params): return None # AST transformer fallback if self.statement_config.parameter_config.ast_transformer is not None and any(p is None for p in params): return None - # Fast-path: skip qc_rebind entirely when no transformations are needed. + # Fast-path: skip stmt_cache_rebind entirely when no transformations are needed. config = self.statement_config.parameter_config needs_rebind = bool(cached.input_named_parameters or cached.applied_wrap_types) if not needs_rebind and config.type_coercion_map: @@ -1025,7 +1025,7 @@ def _qc_prepare_direct(self, statement: str, params: "tuple[Any, ...] | list[Any else: needs_rebind = any(type(p) in coercion_types for p in params) if needs_rebind: - rebound_params = self.qc_rebind(params, cached) + rebound_params = self.stmt_cache_rebind(params, cached) params_are_simple = False else: rebound_params = params @@ -1037,11 +1037,11 @@ def _qc_prepare_direct(self, statement: str, params: "tuple[Any, ...] | list[Any compiled_sql, rebound_params = output_transformer(compiled_sql, rebound_params) params_are_simple = False - return self._qc_build_direct( + return self._stmt_cache_build_direct( statement, params, cached, rebound_params, params_are_simple=params_are_simple, compiled_sql=compiled_sql ) - def _qc_lookup( + def _stmt_cache_lookup( self, statement: str, params: "tuple[Any, ...] | list[Any]" ) -> "SQLResult | None | Awaitable[SQLResult | None]": """Attempt cached execution for query. @@ -1054,9 +1054,9 @@ def _qc_lookup( SQLResult (sync) or Awaitable[SQLResult] (async) if cache hit, None if cache miss. """ - if not self._qc_enabled: + if not self._stmt_cache_enabled: return None - cached = self._qc.get(statement) + cached = self._stmt_cache.get(statement) if cached is None or cached.param_count != len(params): return None @@ -1077,11 +1077,11 @@ def _qc_lookup( needs_rebind = any(type(p) in coercion_types for p in params) if not needs_rebind and not config._has_output_transformer: # pyright: ignore[reportPrivateUsage] - return self._qc_execute_direct(statement, params, cached) + return self._stmt_cache_execute_direct(statement, params, cached) # Fallback to standard path (builds SQL object) if needs_rebind: - rebound_params = self.qc_rebind(params, cached) + rebound_params = self.stmt_cache_rebind(params, cached) params_are_simple = False else: rebound_params = params @@ -1092,29 +1092,29 @@ def _qc_lookup( compiled_sql, rebound_params = config.output_transformer(compiled_sql, rebound_params) # type: ignore[misc] params_are_simple = False - prepared = self._qc_build_direct( + prepared = self._stmt_cache_build_direct( statement, params, cached, rebound_params, params_are_simple=params_are_simple, compiled_sql=compiled_sql ) - return self._qc_execute(prepared) + return self._stmt_cache_execute(prepared) - def _qc_execute(self, statement: "SQL") -> "SQLResult | Awaitable[SQLResult]": + def _stmt_cache_execute(self, statement: "SQL") -> "SQLResult | Awaitable[SQLResult]": raise NotImplementedError - def _qc_execute_direct( + def _stmt_cache_execute_direct( self, sql: str, params: "tuple[Any, ...] | list[Any]", cached: "CachedQuery" ) -> "SQLResult | Awaitable[SQLResult]": """Execute pre-compiled query via ultra-fast path (no rebind needed). Default implementation falls back to building a direct SQL object and - routing through _qc_execute. Subclasses may override with adapter-specific + routing through _stmt_cache_execute. Subclasses may override with adapter-specific implementations that bypass SQL object construction entirely. """ - prepared = self._qc_build_direct( + prepared = self._stmt_cache_build_direct( sql, params, cached, params, params_are_simple=True, compiled_sql=cached.compiled_sql ) - return self._qc_execute(prepared) + return self._stmt_cache_execute(prepared) - def _qc_store(self, statement: "SQL") -> None: + def _stmt_cache_store(self, statement: "SQL") -> None: """Store statement in cache if eligible. All eligibility validation happens here (executed once per unique query). @@ -1129,7 +1129,7 @@ def _qc_store(self, statement: "SQL") -> None: - Filtered statements (dynamic WHERE clauses) - Unprocessed statements (no compiled metadata) """ - if not self._qc_enabled: + if not self._stmt_cache_enabled: return if statement.statement_config is not self.statement_config: return @@ -1184,7 +1184,7 @@ def _qc_store(self, statement: "SQL") -> None: param_count=param_profile.total_count, processed_state=cached_state, ) - self._qc.set(statement.raw_sql, cached) + self._stmt_cache.set(statement.raw_sql, cached) @overload @staticmethod @@ -1301,7 +1301,7 @@ def build_statement_result(self, statement: "SQL", execution_result: ExecutionRe operation_type="SCRIPT", total_statements=execution_result.statement_count or 0, successful_statements=execution_result.successful_statements or 0, - metadata=execution_result.special_data or _DEFAULT_METADATA, + metadata=execution_result.special_data or _DEFAULT_DML_METADATA, ) if execution_result.is_select_result: @@ -1315,15 +1315,15 @@ def build_statement_result(self, statement: "SQL", execution_result: ExecutionRe row_format=execution_result.row_format, ) - # DML path (INSERT/UPDATE/DELETE): use _EMPTY_DATA sentinel to avoid + # DML path (INSERT/UPDATE/DELETE): use _EMPTY_DML_DATA sentinel to avoid # allocating a new empty list on every DML execution. return SQLResult( statement=statement, - data=_EMPTY_DATA, + data=_EMPTY_DML_DATA, rows_affected=execution_result.rowcount_override or 0, operation_type=statement.operation_type, last_inserted_id=execution_result.last_inserted_id, - metadata=execution_result.special_data or _DEFAULT_METADATA, + metadata=execution_result.special_data or _DEFAULT_DML_METADATA, ) def _should_force_select(self, statement: "SQL", cursor: object) -> bool: @@ -1785,14 +1785,14 @@ def _get_compiled_statement( cached_statement = CachedStatement( compiled_sql=compiled_sql, parameters=prepared_parameters, expression=statement.expression ) - self._qc_store(statement) + self._stmt_cache_store(statement) return cached_statement, prepared_parameters processed = statement.get_processed_state() - # DIRECT FAST PATH: When _qc_is_direct is set, execution_parameters + # DIRECT FAST PATH: When _is_cache_direct is set, execution_parameters # already went through prepare_driver_parameters. # Skip the redundant parameter processing and store entirely. - if getattr(statement, "_qc_is_direct", False): + if getattr(statement, "_is_cache_direct", False): prepared_parameters = processed.execution_parameters cached_statement = CachedStatement( compiled_sql=processed.compiled_sql, @@ -1812,7 +1812,7 @@ def _get_compiled_statement( parameters=prepared_parameters, expression=processed.parsed_expression, ) - self._qc_store(statement) + self._stmt_cache_store(statement) return cached_statement, prepared_parameters # Materialize iterators before cache key generation to prevent exhaustion. @@ -1851,7 +1851,7 @@ def _get_compiled_statement( parameters=prepared_parameters, expression=cached_result.expression, ) - self._qc_store(statement) + self._stmt_cache_store(statement) return updated_cached, prepared_parameters # Compile the statement directly (no need for prepare_statement indirection) @@ -1869,7 +1869,7 @@ def _get_compiled_statement( if cache_key is not None and cache is not None: cache.put_statement(cache_key, cached_statement, dialect_key) - self._qc_store(statement) + self._stmt_cache_store(statement) return cached_statement, prepared_parameters def _generate_compilation_cache_key( diff --git a/sqlspec/driver/_query_cache.py b/sqlspec/driver/_query_cache.py index a70626f3..a7f2decb 100644 --- a/sqlspec/driver/_query_cache.py +++ b/sqlspec/driver/_query_cache.py @@ -8,9 +8,9 @@ from sqlspec.core.parameters import ParameterProfile from sqlspec.core.statement import ProcessedState -__all__ = ("QC_MAX_SIZE", "CachedQuery", "QueryCache") +__all__ = ("STMT_CACHE_MAX_SIZE", "CachedQuery", "QueryCache") -QC_MAX_SIZE: Final[int] = 1024 +STMT_CACHE_MAX_SIZE: Final[int] = 1024 class CachedQuery: @@ -70,7 +70,7 @@ class QueryCache: __slots__ = ("_cache", "_max_size") - def __init__(self, max_size: int = QC_MAX_SIZE) -> None: + def __init__(self, max_size: int = STMT_CACHE_MAX_SIZE) -> None: self._cache: OrderedDict[str, CachedQuery] = OrderedDict() self._max_size = max_size diff --git a/sqlspec/driver/_sync.py b/sqlspec/driver/_sync.py index 387e3a22..c7e2db3d 100644 --- a/sqlspec/driver/_sync.py +++ b/sqlspec/driver/_sync.py @@ -345,7 +345,9 @@ def resolve_rowcount(self, cursor: Any) -> int: msg = "Adapter must implement resolve_rowcount() for direct execution path" raise NotImplementedError(msg) - 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 pre-compiled query via ultra-fast path (sync). Uses a DB-API direct path when available (`cursor.execute`) and falls @@ -379,7 +381,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, @@ -396,7 +398,7 @@ def _qc_execute_direct(self, sql: str, params: "tuple[Any, ...] | list[Any]", ca # Fall back to adapter dispatch path. pass - 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 ) execution_result = self.dispatch_execute(cursor, direct_statement) @@ -422,10 +424,10 @@ def _qc_execute_direct(self, sql: str, params: "tuple[Any, ...] | list[Any]", ca msg = "unreachable" raise AssertionError(msg) # pragma: no cover - all paths return or raise - def _qc_execute(self, statement: "SQL") -> "SQLResult": + def _stmt_cache_execute(self, statement: "SQL") -> "SQLResult": """Execute pre-compiled query via fast path. - The statement is already compiled by _qc_prepare, so dispatch_execute + The statement is already compiled by _stmt_cache_prepare_direct, so dispatch_execute will hit the fast path in _get_compiled_statement (is_processed check). """ exc_handler = self.handle_database_exceptions() @@ -495,14 +497,14 @@ def execute( ) -> "SQLResult": """Execute a statement with parameter handling.""" if ( - self._qc_enabled + self._stmt_cache_enabled and (statement_config is None or statement_config is self.statement_config) and isinstance(statement, str) and len(parameters) == 1 and isinstance(parameters[0], (tuple, list)) and not kwargs ): - fast_result = self._qc_lookup(statement, parameters[0]) + fast_result = self._stmt_cache_lookup(statement, parameters[0]) if fast_result is not None: return fast_result # type: ignore[return-value] sql_statement = self.prepare_statement( diff --git a/tests/unit/adapters/test_sync_adapters.py b/tests/unit/adapters/test_sync_adapters.py index 4ce4e8e3..a984b238 100644 --- a/tests/unit/adapters/test_sync_adapters.py +++ b/tests/unit/adapters/test_sync_adapters.py @@ -44,7 +44,7 @@ def test_sync_driver_with_custom_config(mock_sync_connection: MockSyncConnection def test_sync_driver_fast_path_flag_default(mock_sync_connection: MockSyncConnection) -> None: driver = MockSyncDriver(mock_sync_connection) - assert driver._qc_enabled is True + assert driver._stmt_cache_enabled is True def test_sync_driver_fast_path_flag_disabled_by_transformer(mock_sync_connection: MockSyncConnection) -> None: @@ -60,7 +60,7 @@ def transformer(expression: Any, context: Any) -> "tuple[Any, Any]": ) driver = MockSyncDriver(mock_sync_connection, custom_config) - assert driver._qc_enabled is False + assert driver._stmt_cache_enabled is False def test_sync_driver_fast_path_flag_disabled_by_observability(mock_sync_connection: MockSyncConnection) -> None: @@ -69,7 +69,7 @@ def test_sync_driver_fast_path_flag_disabled_by_observability(mock_sync_connecti driver.attach_observability(runtime) - assert driver._qc_enabled is False + assert driver._stmt_cache_enabled is False def test_sync_driver_with_cursor(mock_sync_driver: MockSyncDriver) -> None: diff --git a/tests/unit/driver/test_query_cache.py b/tests/unit/driver/test_query_cache.py index 4e2ea75e..181ed52c 100644 --- a/tests/unit/driver/test_query_cache.py +++ b/tests/unit/driver/test_query_cache.py @@ -41,11 +41,11 @@ def _make_cached( class _FakeDriver(CommonDriverAttributesMixin): __slots__ = () - def _qc_execute(self, statement: Any) -> Any: + def _stmt_cache_execute(self, statement: Any) -> Any: return statement -def test_qc_lru_eviction() -> None: +def test_stmt_cache_lru_eviction() -> None: cache = QueryCache(max_size=2) cache.set("a", _make_cached("SQL_A", 1)) @@ -59,7 +59,7 @@ def test_qc_lru_eviction() -> None: assert cache.get("c") is not None -def test_qc_update_moves_to_end() -> None: +def test_stmt_cache_update_moves_to_end() -> None: cache = QueryCache(max_size=2) cache.set("a", _make_cached("SQL_A", 1)) @@ -74,7 +74,7 @@ def test_qc_update_moves_to_end() -> None: assert entry.param_count == 2 -def test_qc_lookup_cache_hit_rebinds() -> None: +def test_stmt_cache_lookup_cache_hit_rebinds() -> None: config = StatementConfig( parameter_config=ParameterStyleConfig( default_parameter_style=ParameterStyle.QMARK, supported_parameter_styles={ParameterStyle.QMARK} @@ -95,9 +95,9 @@ def test_qc_lookup_cache_hit_rebinds() -> None: param_count=1, processed_state=ps, ) - driver._qc.set("SELECT * FROM t WHERE id = ?", cached) + driver._stmt_cache.set("SELECT * FROM t WHERE id = ?", cached) - result = driver._qc_lookup("SELECT * FROM t WHERE id = ?", (1,)) + result = driver._stmt_cache_lookup("SELECT * FROM t WHERE id = ?", (1,)) assert result is not None # Result is the SQL statement with processed state @@ -108,7 +108,7 @@ def test_qc_lookup_cache_hit_rebinds() -> None: assert params == (1,) -def test_qc_store_snapshots_processed_state() -> None: +def test_stmt_cache_store_snapshots_processed_state() -> None: config = StatementConfig( parameter_config=ParameterStyleConfig( default_parameter_style=ParameterStyle.QMARK, supported_parameter_styles={ParameterStyle.QMARK} @@ -118,8 +118,8 @@ def test_qc_store_snapshots_processed_state() -> None: statement = SQL("SELECT ?", (1,), statement_config=config) statement.compile() - driver._qc_store(statement) - cached = driver._qc.get("SELECT ?") + driver._stmt_cache_store(statement) + cached = driver._stmt_cache.get("SELECT ?") assert cached is not None # Mutate/reset the original state after cache storage; cached metadata @@ -167,7 +167,7 @@ def test_prepare_driver_parameters_many_coerces_rows_when_needed() -> None: assert tuple(prepared[1]) == ("b",) -def test_sync_qc_execute_direct_uses_dispatch_path(mock_sync_driver, monkeypatch) -> None: +def test_sync_stmt_cache_execute_direct_uses_dispatch_path(mock_sync_driver, monkeypatch) -> None: class _CursorManager: def __enter__(self) -> object: return object() @@ -180,7 +180,7 @@ def _fake_with_cursor(_connection: Any) -> _CursorManager: return _CursorManager() def _fake_dispatch_execute(cursor: Any, statement: Any) -> Any: - # Regression test: direct QC execution should not require cursor.execute(). + # Regression test: direct cache execution should not require cursor.execute(). assert not hasattr(cursor, "execute") return mock_sync_driver.create_execution_result(cursor, rowcount_override=7) @@ -197,7 +197,7 @@ def _fake_dispatch_execute(cursor: Any, statement: Any) -> Any: ), ) - result = mock_sync_driver._qc_execute_direct("INSERT INTO t (id) VALUES (?)", (1,), cached) + result = mock_sync_driver._stmt_cache_execute_direct("INSERT INTO t (id) VALUES (?)", (1,), cached) assert result.operation_type == "INSERT" assert result.rows_affected == 7 @@ -210,8 +210,8 @@ def _fake_try(statement: str, params: tuple[Any, ...] | list[Any]) -> object: called["args"] = (statement, params) return sentinel - monkeypatch.setattr(mock_sync_driver, "_qc_lookup", _fake_try) - mock_sync_driver._qc_enabled = True + monkeypatch.setattr(mock_sync_driver, "_stmt_cache_lookup", _fake_try) + mock_sync_driver._stmt_cache_enabled = True result = mock_sync_driver.execute("SELECT ?", (1,)) @@ -227,8 +227,8 @@ def _fake_try(statement: str, params: tuple[Any, ...] | list[Any]) -> object: called = True return object() - monkeypatch.setattr(mock_sync_driver, "_qc_lookup", _fake_try) - mock_sync_driver._qc_enabled = True + monkeypatch.setattr(mock_sync_driver, "_stmt_cache_lookup", _fake_try) + mock_sync_driver._stmt_cache_enabled = True statement_config = mock_sync_driver.statement_config.replace() result = mock_sync_driver.execute("SELECT ?", (1,), statement_config=statement_config) @@ -238,13 +238,13 @@ def _fake_try(statement: str, params: tuple[Any, ...] | list[Any]) -> object: def test_execute_populates_fast_path_cache_on_normal_path(mock_sync_driver) -> None: - mock_sync_driver._qc_enabled = True + mock_sync_driver._stmt_cache_enabled = True - assert mock_sync_driver._qc.get("SELECT ?") is None + assert mock_sync_driver._stmt_cache.get("SELECT ?") is None result = mock_sync_driver.execute("SELECT ?", (1,)) - cached = mock_sync_driver._qc.get("SELECT ?") + cached = mock_sync_driver._stmt_cache.get("SELECT ?") assert cached is not None assert cached.param_count == 1 assert cached.operation_type == "SELECT" @@ -260,8 +260,8 @@ async def _fake_try(statement: str, params: tuple[Any, ...] | list[Any]) -> obje called["args"] = (statement, params) return sentinel - monkeypatch.setattr(mock_async_driver, "_qc_lookup", _fake_try) - mock_async_driver._qc_enabled = True + monkeypatch.setattr(mock_async_driver, "_stmt_cache_lookup", _fake_try) + mock_async_driver._stmt_cache_enabled = True result = await mock_async_driver.execute("SELECT ?", (1,)) @@ -278,8 +278,8 @@ async def _fake_try(statement: str, params: tuple[Any, ...] | list[Any]) -> obje called = True return object() - monkeypatch.setattr(mock_async_driver, "_qc_lookup", _fake_try) - mock_async_driver._qc_enabled = True + monkeypatch.setattr(mock_async_driver, "_stmt_cache_lookup", _fake_try) + mock_async_driver._stmt_cache_enabled = True statement_config = mock_async_driver.statement_config.replace() result = await mock_async_driver.execute("SELECT ?", (1,), statement_config=statement_config) @@ -290,20 +290,20 @@ async def _fake_try(statement: str, params: tuple[Any, ...] | list[Any]) -> obje @pytest.mark.asyncio async def test_async_execute_populates_fast_path_cache_on_normal_path(mock_async_driver) -> None: - mock_async_driver._qc_enabled = True + mock_async_driver._stmt_cache_enabled = True - assert mock_async_driver._qc.get("SELECT ?") is None + assert mock_async_driver._stmt_cache.get("SELECT ?") is None result = await mock_async_driver.execute("SELECT ?", (1,)) - cached = mock_async_driver._qc.get("SELECT ?") + cached = mock_async_driver._stmt_cache.get("SELECT ?") assert cached is not None assert cached.param_count == 1 assert cached.operation_type == "SELECT" assert result.operation_type == "SELECT" -def test_qc_thread_safety() -> None: +def test_stmt_cache_thread_safety() -> None: cache = QueryCache(max_size=32) cached = _make_cached() for idx in range(16): diff --git a/tools/scripts/analyze_profile.py b/tools/scripts/analyze_profile.py index 9eb2a89b..816d93c9 100644 --- a/tools/scripts/analyze_profile.py +++ b/tools/scripts/analyze_profile.py @@ -179,11 +179,11 @@ def _print_per_execute_summary(stats: pstats.Stats, console: Console) -> None: "build_statement_result", "prepare_driver_parameters", "_format_parameter_set", - "_qc_prepare", - "_qc_lookup", - "_qc_build", - "_qc_execute", - "_qc_store", + "_stmt_cache_prepare", + "_stmt_cache_lookup", + "_stmt_cache_build", + "_stmt_cache_execute", + "_stmt_cache_store", "process_parameters", "execute", "cursor", diff --git a/tools/scripts/bench_subsystems.py b/tools/scripts/bench_subsystems.py index 1f021bc7..64284b43 100644 --- a/tools/scripts/bench_subsystems.py +++ b/tools/scripts/bench_subsystems.py @@ -206,40 +206,40 @@ def bench_compile_complex() -> None: session.execute("INSERT INTO test (value) VALUES (?)", ("cache_prime2",)) # Now benchmark the direct prepare path - def bench_qc_prepare_hit() -> None: - session._qc_prepare_direct("INSERT INTO test (value) VALUES (?)", ("bench_val",)) + def bench_cache_prepare_hit() -> None: + session._stmt_cache_prepare_direct("INSERT INTO test (value) VALUES (?)", ("bench_val",)) benchmarks.append( SubsystemBenchmark( - name="QC _qc_prepare_direct() - cache hit", - bench_fn=bench_qc_prepare_hit, + name="QC _stmt_cache_prepare_direct() - cache hit", + bench_fn=bench_cache_prepare_hit, iterations=iterations, description="Direct prepare with cache hit", ) ) - def bench_qc_prepare_miss() -> None: - session._qc_prepare_direct("INSERT INTO unique_table (col) VALUES (?)", ("val",)) + def bench_cache_prepare_miss() -> None: + session._stmt_cache_prepare_direct("INSERT INTO unique_table (col) VALUES (?)", ("val",)) benchmarks.append( SubsystemBenchmark( - name="QC _qc_prepare_direct() - cache miss", - bench_fn=bench_qc_prepare_miss, + name="QC _stmt_cache_prepare_direct() - cache miss", + bench_fn=bench_cache_prepare_miss, iterations=iterations, description="Direct prepare with cache miss", ) ) - # Full QC lookup cycle - def bench_qc_lookup() -> None: - session._qc_lookup("INSERT INTO test (value) VALUES (?)", ("bench_val",)) + # Full statement cache lookup cycle + def bench_stmt_cache_lookup() -> None: + session._stmt_cache_lookup("INSERT INTO test (value) VALUES (?)", ("bench_val",)) benchmarks.append( SubsystemBenchmark( - name="QC _qc_lookup() - full cycle", - bench_fn=bench_qc_lookup, + name="QC _stmt_cache_lookup() - full cycle", + bench_fn=bench_stmt_cache_lookup, iterations=iterations, - description="Full QC lookup -> prepare -> execute cycle", + description="Full stmt_cache lookup -> prepare -> execute cycle", ) ) From 70e0b6b5cabf88d994dbbe03efd544402c5aba0c Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sun, 22 Feb 2026 16:40:03 +0000 Subject: [PATCH 4/4] fix(types): use inline empty lists for mypyc-compatible DMLResult fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace tuple sentinels with inline empty lists for column_names, inserted_ids, statement_results, and errors in FastDMLResult to fix mypyc compilation — mypyc generates C struct slots typed as list, so assigning tuples causes type errors during wheel builds. --- sqlspec/core/result/_base.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/sqlspec/core/result/_base.py b/sqlspec/core/result/_base.py index 5743b420..1c0c6dcd 100644 --- a/sqlspec/core/result/_base.py +++ b/sqlspec/core/result/_base.py @@ -50,10 +50,6 @@ _EMPTY_RESULT_STATEMENT = SQL("-- empty stack result --") _EMPTY_RESULT_DATA: "tuple[()]" = () _DEFAULT_DML_METADATA: dict[str, Any] = {} -_EMPTY_DML_COLUMN_NAMES: "tuple[()]" = () -_EMPTY_DML_INSERTED_IDS: "tuple[()]" = () -_EMPTY_DML_STATEMENT_RESULTS: "tuple[()]" = () -_EMPTY_DML_ERRORS: "tuple[()]" = () _TWO_COLUMN_THRESHOLD = 2 @@ -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