diff --git a/Makefile b/Makefile index eb8b8270c..6024178bd 100644 --- a/Makefile +++ b/Makefile @@ -346,10 +346,22 @@ bench-gate: ## Run performance regressio DRIVERS ?= duckdb aiosqlite bench-gate-adapters: ## Compare selected adapter benchmarks with committed baselines @mkdir -p /tmp/sqlspec-bench - @for driver in ${DRIVERS}; do \ - uv run python tools/scripts/bench.py --driver "$${driver}" --extended --json-output "/tmp/sqlspec-bench/$${driver}.json"; \ - uv run python tools/scripts/bench_compare.py "tools/perf_baselines/$${driver}.json" "/tmp/sqlspec-bench/$${driver}.json" --threshold "$${BENCHMARK_THRESHOLD:-30}"; \ - done + @failed_drivers=""; \ + for driver in ${DRIVERS}; do \ + baseline="tools/perf_baselines/$${driver}.json"; \ + run_args=$$(uv run --no-sync python tools/scripts/bench_compare.py "$${baseline}" --print-run-args); \ + if ! uv run --no-sync python tools/scripts/bench.py --driver "$${driver}" --extended $${run_args} --json-output "/tmp/sqlspec-bench/$${driver}.json"; then \ + failed_drivers="$${failed_drivers} $${driver}"; \ + continue; \ + fi; \ + if ! uv run --no-sync python tools/scripts/bench_compare.py "$${baseline}" "/tmp/sqlspec-bench/$${driver}.json" --mode "$${BENCHMARK_MODE:-ratio}" --threshold "$${BENCHMARK_THRESHOLD:-30}"; then \ + failed_drivers="$${failed_drivers} $${driver}"; \ + fi; \ + done; \ + if [ -n "$${failed_drivers}" ]; then \ + echo "Adapter benchmark gate failed for:$${failed_drivers}"; \ + exit 1; \ + fi .PHONY: bench-subsystems bench-subsystems: ## Run subsystem micro-benchmarks diff --git a/docs/changelog.rst b/docs/changelog.rst index 926f49291..4f0e3c321 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,6 +9,74 @@ important operational fixes. Recent Updates ============== +v0.67.0 - Spanner graph and hybrid search, ADK artifact stores, and Python 3.11+ baseline +----------------------------------------------------------------------------------------- + +**Added:** + +* Spanner and Spangres SQLGlot dialects support ``CREATE PROPERTY GRAPH`` and + ``CREATE OR REPLACE PROPERTY GRAPH`` DDL (with ``NODE TABLES``, ``EDGE TABLES``, + ``SOURCE KEY``, ``DESTINATION KEY``, ``LABEL``, and ``PROPERTIES`` clauses) as + well as ``GRAPH_TABLE`` queries with ``MATCH`` path patterns and ``COLUMNS`` + projections. + (`#834 `_) +* Added ``sql.hybrid_rrf(...)`` on the ``SQL`` builder factory to generate + Reciprocal Rank Fusion (RRF) hybrid search queries combining full-text BM25 + scoring and vector distance ranking on Spanner and Spangres. + (`#834 `_) +* Added Google ADK artifact store base classes (``BaseAsyncADKArtifactStore`` + and ``BaseSyncADKArtifactStore``) and Spanner implementations + (``SpannerSyncADKArtifactStore`` and ``SpannerAsyncADKArtifactStore``) with + optional Cloud Storage URI offloading for large payloads. + (`#834 `_) +* Spanner event queue stores add a composite queue index + ``(channel, status, available_at)`` with ``FORCE_INDEX`` query hints, row + deletion TTL policies, and single-statement ``THEN RETURN`` claim execution. + (`#834 `_) +* Added Python 3.15 support across package metadata and CI test workflows. + (`#835 `_) +* Added pure-Go GizmoData ADBC driver support in ``sqlspec.adapters.adbc`` and + extension stores (``ADBCStore``, ``AdbcEventQueueStore``, ``AdbcADKStore``, + and ``AdbcADKMemoryStore``) for Oracle (``adbc-driver-oracle``), IBM Db2 + (``adbc-driver-db2``), native GizmoSQL (``adbc-driver-gizmosql`` with + ``auth_type="external"`` OAuth SSO), and DuckDB Quack (``adbc-driver-quack``). + (`#839 `_) + +**Changed:** + +* Upgraded the minimum supported Python version to 3.11+ and modernized runtime + typing, ``datetime.UTC``, and ``asyncio.timeout`` usage across the codebase. + (`#835 `_) +* Standardized driver session lifecycles, statement compilation caching, SQLGlot + builder immutability, migration version tracking, and ADK/Litestar/Events + store contracts across all 20 database adapters. + (`#837 `_) +* Unified DuckDB row materialization across ``execute()``, ``select()``, and + ``fetch*`` methods to consistently return DuckDB-native ``fetchall()`` Python + types on both first and cached executions, while ``select_to_arrow()`` + continues to expose raw Arrow tables. + (`#838 `_) +* Reduced per-statement and per-session execution overhead with per-config shared + statement caches (``StatementCacheHolder``), zero-copy cached SQLGlot AST + sharing with copy-on-mutation via ``ensure_owned_expression()``, fast-path + execution for parameterless and scalar-positional calls, fast-path execution + when statement observers are active, lazy ``SQLResult.statement`` + materialization on cached ``SELECT`` hits, single-pass ``execute_many()`` + parameter preparation, and idle-queue/clean-transaction fast paths in + ``AiosqliteConnectionPool``. + (`#838 `_) + +**Fixed:** + +* Hardened parameter conversion, arrow batch streaming, transaction rollback and + cleanup handling, exception mapping, and dialect generation across core, + storage, migrations, extensions, and database adapters. + (`#837 `_) +* Prevented unparsed or ``COMMAND``/``UNKNOWN`` fallback compilations from + populating the driver statement cache, and cached parsed target-table metadata + on DuckDB Arrow bulk ``execute_many()`` inserts. + (`#838 `_) + v0.66.0 - Native AsyncIO Spanner adapter, extension stores, and sync/async parity --------------------------------------------------------------------------------- diff --git a/docs/conf.py b/docs/conf.py index e3603299e..d7b7e6f96 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,7 +65,6 @@ "auto_pytabs.sphinx_ext", "sphinx_copybutton", "sphinx_click", - "click_extra.sphinx", "sphinx_design", "sphinx_paramlinks", "sphinxcontrib.mermaid", @@ -73,6 +72,7 @@ "sphinx_iconify", "tools.sphinx_ext.playground", ] +myst_enable_extensions = ["alert", "colon_fence"] intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "litestar": ("https://docs.litestar.dev/latest/", None), @@ -80,7 +80,7 @@ "anyio": ("https://anyio.readthedocs.io/en/stable/", None), "multidict": ("https://multidict.aio-libs.org/en/stable/", None), "cryptography": ("https://cryptography.io/en/latest/", None), - "pydantic": ("https://docs.pydantic.dev/latest/", None), + "pydantic": ("https://pydantic.dev/docs/validation/latest/", None), "sanic": ("https://sanic.readthedocs.io/en/latest/", None), "flask": ("https://flask.palletsprojects.com/en/stable/", None), "fastapi": ("https://fastapi.tiangolo.com/", None), diff --git a/pyproject.toml b/pyproject.toml index 663a16f59..f1265960b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ maintainers = [{ name = "Litestar Developers", email = "hello@litestar.dev" }] name = "sqlspec" readme = "README.md" requires-python = ">=3.11, <4.0" -version = "0.66.0" +version = "0.67.0" [project.urls] Discord = "https://discord.gg/litestar" @@ -336,7 +336,7 @@ opt_level = "3" # Maximum optimization (0-3) allow_dirty = true commit = false commit_args = "--no-verify" -current_version = "0.66.0" +current_version = "0.67.0" ignore_missing_files = false ignore_missing_version = false message = "chore(release): bump to v{new_version}" diff --git a/sqlspec/adapters/aiosqlite/driver.py b/sqlspec/adapters/aiosqlite/driver.py index d41d3963c..fc90c5277 100644 --- a/sqlspec/adapters/aiosqlite/driver.py +++ b/sqlspec/adapters/aiosqlite/driver.py @@ -133,7 +133,7 @@ async def dispatch_execute(self, cursor: "AiosqliteRawCursor", statement: "SQL") sql, normalized_parameters, statement.operation_type, - statement.expression, + statement._peek_expression(), self._rowid_target_cache, ) data, column_names, row_count = collect_rows(fetched_data, description) @@ -156,7 +156,7 @@ async def dispatch_execute(self, cursor: "AiosqliteRawCursor", statement: "SQL") sql, normalized_parameters, statement.operation_type, - statement.expression, + statement._peek_expression(), self._rowid_target_cache, ) return self.create_execution_result(cursor, rowcount_override=affected_rows, last_inserted_id=last_inserted_id) @@ -389,56 +389,48 @@ async def _execute_cache_hit( ) -> "SQLResult": """Execute cached queries through the async cursor fast path.""" normalized_parameters = normalize_execute_parameters(params) - direct_statement: SQL | None = None exc_handler = self.handle_database_exceptions() result: SQLResult | None = None self._invalidate_rowid_target_cache(cached.operation_type) - try: - async with exc_handler: - if cached.operation_profile.returns_rows: - fetched_data, description, _affected_rows, last_inserted_id = await run_on_worker_thread( - self.connection, - _execute_fetchall_with_metadata, - self.connection, - cached.compiled_sql, - normalized_parameters, - cached.operation_type, - cached.processed_state.parsed_expression, - self._rowid_target_cache, - ) - data, column_names, row_count = collect_rows(fetched_data, description) - execution_result = self.create_execution_result( - self.connection, - selected_data=data, - column_names=column_names, - data_row_count=row_count, - is_select_result=True, - row_format=resolve_row_format(data), - last_inserted_id=last_inserted_id, - ) - direct_statement = self._cached_statement( - sql, params, cached, params, params_are_simple=True, compiled_sql=cached.compiled_sql - ) - result = self.build_statement_result(direct_statement, execution_result) - else: - affected_rows, last_inserted_id = await run_on_worker_thread( - self.connection, - _execute_and_resolve_metadata, - self.connection, - cached.compiled_sql, - normalized_parameters, - cached.operation_type, - cached.processed_state.parsed_expression, - self._rowid_target_cache, - ) - result = DMLResult(cached.operation_type, affected_rows, last_inserted_id) - - self._check_pending_exception(exc_handler) - assert result is not None - return result - finally: - if direct_statement is not None: - self._release_pooled_statement(direct_statement) + async with exc_handler: + if cached.operation_profile.returns_rows: + fetched_data, description, _affected_rows, last_inserted_id = await run_on_worker_thread( + self.connection, + _execute_fetchall_with_metadata, + self.connection, + cached.compiled_sql, + normalized_parameters, + cached.operation_type, + cached.processed_state.parsed_expression, + self._rowid_target_cache, + ) + data, column_names, row_count = collect_rows(fetched_data, description) + result = self.build_cached_select_result( + sql, + params, + cached, + selected_data=data, + column_names=column_names, + data_row_count=row_count, + row_format=resolve_row_format(data), + last_inserted_id=last_inserted_id, + ) + else: + affected_rows, last_inserted_id = await run_on_worker_thread( + self.connection, + _execute_and_resolve_metadata, + self.connection, + cached.compiled_sql, + normalized_parameters, + cached.operation_type, + cached.processed_state.parsed_expression, + self._rowid_target_cache, + ) + result = DMLResult(cached.operation_type, affected_rows, last_inserted_id) + + self._check_pending_exception(exc_handler) + assert result is not None + return result def _invalidate_rowid_target_cache(self, operation_type: "OperationType") -> None: if operation_type not in {"SELECT", "INSERT", "UPDATE", "DELETE"}: diff --git a/sqlspec/adapters/aiosqlite/pool.py b/sqlspec/adapters/aiosqlite/pool.py index 72dfb9f67..2c166cc9f 100644 --- a/sqlspec/adapters/aiosqlite/pool.py +++ b/sqlspec/adapters/aiosqlite/pool.py @@ -382,6 +382,9 @@ async def acquire(self) -> AiosqlitePoolConnection: Raises: AiosqliteConnectTimeoutError: If acquisition times out """ + fast_connection = self._try_acquire_idle_connection() + if fast_connection is not None: + return fast_connection try: connection = await asyncio.wait_for(self._get_connection(), timeout=self._connect_timeout) except AiosqlitePoolClosedError: @@ -414,8 +417,10 @@ async def release(self, connection: AiosqlitePoolConnection) -> None: return try: - with suppress(Exception): - await end_transaction(connection.connection, commit=False) + raw_conn = connection.connection + if getattr(raw_conn, "in_transaction", False): + with suppress(Exception): + await end_transaction(raw_conn, commit=False) connection.idle_since = time.time() self._queue.put_nowait(connection) except Exception as e: @@ -796,6 +801,20 @@ async def _warm_pool(self) -> None: async with self._lock: self._in_flight_creations -= connections_needed + def _try_acquire_idle_connection(self) -> "AiosqlitePoolConnection | None": + """Synchronously claim a warm, recently-idle connection when available.""" + if self._closed_event.is_set() or (not self._warmed and self._min_size > 0) or self._queue.empty(): + return None + connection = self._queue.get_nowait() + idle_since = connection.idle_since + if idle_since is not None and connection.is_healthy: + idle_time = time.time() - idle_since + if idle_time <= self._health_check_interval and idle_time <= self._idle_timeout: + connection.idle_since = None + return connection + self._queue.put_nowait(connection) + return None + async def _get_connection(self) -> AiosqlitePoolConnection: """Run the three-phase connection acquisition cycle. diff --git a/sqlspec/adapters/asyncpg/data_dictionary.py b/sqlspec/adapters/asyncpg/data_dictionary.py index 9077b2fc6..33674c6af 100644 --- a/sqlspec/adapters/asyncpg/data_dictionary.py +++ b/sqlspec/adapters/asyncpg/data_dictionary.py @@ -36,7 +36,7 @@ __all__ = ("AsyncpgDataDictionary",) -_POSTGRES_METADATA_DOMAINS = ( +_POSTGRES_METADATA_DOMAINS: tuple[str, ...] = ( "schemas", "objects", "tables", diff --git a/sqlspec/adapters/cockroach_asyncpg/data_dictionary.py b/sqlspec/adapters/cockroach_asyncpg/data_dictionary.py index 1de19ecfb..1335a2f56 100644 --- a/sqlspec/adapters/cockroach_asyncpg/data_dictionary.py +++ b/sqlspec/adapters/cockroach_asyncpg/data_dictionary.py @@ -38,7 +38,7 @@ __all__ = ("CockroachAsyncpgDataDictionary",) -_COCKROACH_METADATA_DOMAINS = ( +_COCKROACH_METADATA_DOMAINS: tuple[str, ...] = ( "schemas", "objects", "tables", diff --git a/sqlspec/adapters/cockroach_psycopg/data_dictionary.py b/sqlspec/adapters/cockroach_psycopg/data_dictionary.py index c58d27320..7891bb7c1 100644 --- a/sqlspec/adapters/cockroach_psycopg/data_dictionary.py +++ b/sqlspec/adapters/cockroach_psycopg/data_dictionary.py @@ -38,7 +38,7 @@ __all__ = ("CockroachPsycopgAsyncDataDictionary", "CockroachPsycopgSyncDataDictionary") -_COCKROACH_METADATA_DOMAINS = ( +_COCKROACH_METADATA_DOMAINS: tuple[str, ...] = ( "schemas", "objects", "tables", diff --git a/sqlspec/adapters/duckdb/core.py b/sqlspec/adapters/duckdb/core.py index d8c756588..69ffcdfd5 100644 --- a/sqlspec/adapters/duckdb/core.py +++ b/sqlspec/adapters/duckdb/core.py @@ -1,6 +1,7 @@ """DuckDB adapter compiled helpers.""" import contextlib +import re from datetime import date, datetime from decimal import Decimal from typing import TYPE_CHECKING, Any, Final, cast @@ -46,12 +47,13 @@ from sqlspec.utils.uuids import uuid_from_string if TYPE_CHECKING: - from collections.abc import Callable, Mapping + from collections.abc import Callable, Mapping, Sequence from sqlspec.storage import StorageDestination, SyncStoragePipeline __all__ = ( + "ARROW_MIN_ROWS", "apply_driver_features", "build_connection_config", "build_profile", @@ -59,12 +61,56 @@ "collect_rows", "create_mapped_exception", "default_statement_config", + "description_has_arrow_divergent_types", "driver_profile", "format_identifier", "normalize_execute_parameters", "resolve_rowcount", ) +ARROW_MIN_ROWS: Final[int] = 64 + +_FIXED_ARRAY_PATTERN: Final[re.Pattern[str]] = re.compile(r"\[\d+\]") +_ARROW_SAFE_SCALAR_TYPES: Final[frozenset[str]] = frozenset({ + "INTEGER", + "BIGINT", + "VARCHAR", + "DOUBLE", + "BOOLEAN", + "DATE", + "TIMESTAMP", + "SMALLINT", + "TINYINT", + "UBIGINT", + "UINTEGER", + "USMALLINT", + "UTINYINT", + "FLOAT", + "BLOB", + "UUID", + "TIME", + "TIMESTAMP WITH TIME ZONE", + "TIMESTAMPTZ", + "TIMESTAMP_S", + "TIMESTAMP_MS", + "JSON", +}) + + +def description_has_arrow_divergent_types(description: "Sequence[Any] | None") -> bool: + """Return whether any column in ``cursor.description`` diverges between Arrow and ``fetchall()``.""" + if not description: + return False + for column in description: + if len(column) <= 1 or column[1] is None: + continue + type_str = str(column[1]).upper() + if type_str in _ARROW_SAFE_SCALAR_TYPES: + continue + if _type_str_is_arrow_divergent(type_str): + return True + return False + def format_identifier(identifier: str) -> str: """Format an identifier with safe double-quote escaping. @@ -178,10 +224,15 @@ def resolve_rowcount(cursor: Any) -> int: Returns: Rowcount value derived from cursor output. """ - with contextlib.suppress(Exception): + try: result = cursor.fetchone() - if result and isinstance(result, tuple) and len(result) == 1: + except Exception: + result = None + if result and isinstance(result, tuple) and len(result) == 1: + try: return int(result[0]) + except (TypeError, ValueError): + result = None if has_rowcount(cursor): return max(cursor.rowcount, 0) return 0 @@ -717,6 +768,25 @@ def _restore_uuid_columns(rows: "list[dict[str, Any]]", description: "list[Any] row[column] = uuid_from_string(value) +def _type_str_is_arrow_divergent(type_str: str) -> bool: + if ( + "HUGEINT" in type_str + or "TIMETZ" in type_str + or "TIME WITH TIME ZONE" in type_str + or "TIMESTAMP_NS" in type_str + or "INTERVAL" in type_str + or "MAP(" in type_str + or "BIGNUM" in type_str + or "VARINT" in type_str + ): + return True + if type_str == "BIT" or "BIT[" in type_str or " BIT" in type_str or type_str.startswith("BIT "): + return True + if "UUID" in type_str and type_str != "UUID": + return True + return "[" in type_str and _FIXED_ARRAY_PATTERN.search(type_str) is not None + + driver_profile = build_profile() default_statement_config = build_statement_config() diff --git a/sqlspec/adapters/duckdb/driver.py b/sqlspec/adapters/duckdb/driver.py index 3c24579ea..6194f7a3f 100644 --- a/sqlspec/adapters/duckdb/driver.py +++ b/sqlspec/adapters/duckdb/driver.py @@ -9,6 +9,7 @@ from sqlspec.adapters.duckdb._typing import DuckDBCursor, DuckDBSessionContext from sqlspec.adapters.duckdb._typing import duckdb_module as duckdb from sqlspec.adapters.duckdb.core import ( + ARROW_MIN_ROWS, _build_storage_copy_sql, _build_storage_read_sql, _DuckDBStreamSource, @@ -17,6 +18,7 @@ collect_rows, create_mapped_exception, default_statement_config, + description_has_arrow_divergent_types, driver_profile, format_identifier, normalize_execute_parameters, @@ -51,8 +53,9 @@ from sqlspec.builder import QueryBuilder from sqlspec.core import ArrowResult, SQLResult, Statement, StatementFilter from sqlspec.driver import ExecutionResult + from sqlspec.driver._query_cache import CachedQuery from sqlspec.storage import StorageBridgeJob, StorageDestination, StorageFormat, StorageTelemetry - from sqlspec.typing import ArrowReturnFormat, StatementParameters + from sqlspec.typing import ArrowReturnFormat, RowFormat, StatementParameters else: pa = import_optional("pyarrow") @@ -143,18 +146,14 @@ def dispatch_execute(self, cursor: "DuckDBConnection", statement: SQL) -> "Execu is_select_like = statement.returns_rows() or self._should_force_select(statement, cursor) if is_select_like: - arrow_table = cursor.to_arrow_table() - data = arrow_table.to_pylist() - _restore_uuid_columns(data, cursor.description) - column_names = list(arrow_table.column_names) - + data, column_names, row_count, row_format = self._materialize_select_rows(cursor, sql) return self.create_execution_result( cursor, selected_data=data, column_names=column_names, - data_row_count=len(data), + data_row_count=row_count, is_select_result=True, - row_format="dict", + row_format=row_format, ) row_count = resolve_rowcount(cursor) @@ -179,8 +178,9 @@ def dispatch_execute_many(self, cursor: "DuckDBConnection", statement: SQL) -> " sql = cached_statement.compiled_sql if prepared_parameters: - parsed_expression = cached_statement.expression or statement.expression + parsed_expression = cached_statement.expression or statement._peek_expression() if isinstance(parsed_expression, exp.Insert) and not parsed_expression.args.get("returning"): + parsed_expression.meta["compiled_sql"] = sql bulk_result = self._execute_bulk_insert_many(parsed_expression, prepared_parameters) if bulk_result is not None: return self.create_execution_result( @@ -559,6 +559,49 @@ def resolve_rowcount(self, cursor: "DuckDBConnection") -> int: """Resolve rowcount from DuckDB cursor for the direct execution path.""" return resolve_rowcount(cursor) + def _execute_cache_hit_with_cursor( + self, sql: str, params: "tuple[Any, ...] | list[Any] | dict[str, Any]", cached: "CachedQuery" + ) -> "SQLResult": + """Execute a cached query using DuckDB's unified row materializer.""" + with self.with_cursor(self.connection) as cursor: + cursor.execute(cached.compiled_sql, normalize_execute_parameters(params)) + if cached.operation_profile.returns_rows: + data, column_names, row_count, row_format = self._materialize_select_rows(cursor, cached.compiled_sql) + return self.build_cached_select_result( + sql, + params, + cached, + selected_data=data, + column_names=column_names, + data_row_count=row_count, + row_format=row_format, + ) + affected_rows = resolve_rowcount(cursor) + return DMLResult(cached.operation_type, affected_rows) + + def _materialize_select_rows(self, cursor: Any, compiled_sql: str) -> "tuple[list[Any], list[str], int, RowFormat]": + """Materialize SELECT rows using DuckDB's native Python type contract.""" + description = cursor.description + hint = self._cache_holder.get_row_count_hint(compiled_sql) + if ( + (hint is not None and hint < ARROW_MIN_ROWS) + or description_has_arrow_divergent_types(description) + or not hasattr(cursor, "to_arrow_table") + ): + fetched_data = cursor.fetchall() + data, column_names = collect_rows(fetched_data, description) + row_count = len(data) + self._cache_holder.set_row_count_hint(compiled_sql, row_count) + return data, column_names, row_count, "tuple" + + arrow_table = cursor.to_arrow_table() + data = arrow_table.to_pylist() + _restore_uuid_columns(data, description) + column_names = list(arrow_table.column_names) + row_count = len(data) + self._cache_holder.set_row_count_hint(compiled_sql, row_count) + return data, column_names, row_count, "dict" + def _native_storage_job( self, sql: str, @@ -607,7 +650,9 @@ def _execute_storage_sql(self, sql: str, parameters: "list[Any]") -> int: raise SQLSpecError(msg) return int(result[0]) - def _execute_bulk_insert_many(self, expression: exp.Insert, prepared_parameters: Any) -> "DMLResult | None": + def _execute_bulk_insert_many( + self, expression: exp.Insert, prepared_parameters: Any, compiled_sql: str = "" + ) -> "DMLResult | None": """Execute a batch INSERT via Arrow registration when the payload is simple.""" if not isinstance(prepared_parameters, list) or not prepared_parameters: return None @@ -632,8 +677,15 @@ def _execute_bulk_insert_many(self, expression: exp.Insert, prepared_parameters: if arrow_table is None: return None - target_table = table_expr.sql(dialect="duckdb") - column_sql = ", ".join(column.sql(dialect="duckdb") for column in expression.this.expressions) + cache_key = compiled_sql or (str(expression._meta.get("compiled_sql", "")) if expression._meta else "") + cached_sql_pair = self._cache_holder.get_bulk_insert_sql(cache_key) if cache_key else None + if cached_sql_pair is not None: + target_table, column_sql = cached_sql_pair + else: + target_table = table_expr.sql(dialect="duckdb") + column_sql = ", ".join(column.sql(dialect="duckdb") for column in expression.this.expressions) + if cache_key: + self._cache_holder.set_bulk_insert_sql(cache_key, (target_table, column_sql)) temp_view = f"_sqlspec_batch_{uuid4().hex}" exc_handler = self.handle_database_exceptions() with exc_handler: diff --git a/sqlspec/adapters/psqlpy/data_dictionary.py b/sqlspec/adapters/psqlpy/data_dictionary.py index 41e0d060f..1f76ef6f2 100644 --- a/sqlspec/adapters/psqlpy/data_dictionary.py +++ b/sqlspec/adapters/psqlpy/data_dictionary.py @@ -38,7 +38,7 @@ __all__ = ("PsqlpyDataDictionary",) -_POSTGRES_METADATA_DOMAINS = ( +_POSTGRES_METADATA_DOMAINS: tuple[str, ...] = ( "schemas", "objects", "tables", diff --git a/sqlspec/adapters/psycopg/data_dictionary.py b/sqlspec/adapters/psycopg/data_dictionary.py index 2bcf3ee5b..2a2a3390b 100644 --- a/sqlspec/adapters/psycopg/data_dictionary.py +++ b/sqlspec/adapters/psycopg/data_dictionary.py @@ -38,7 +38,7 @@ __all__ = ("PsycopgAsyncDataDictionary", "PsycopgSyncDataDictionary") -_POSTGRES_METADATA_DOMAINS = ( +_POSTGRES_METADATA_DOMAINS: tuple[str, ...] = ( "schemas", "objects", "tables", diff --git a/sqlspec/adapters/sqlite/driver.py b/sqlspec/adapters/sqlite/driver.py index 25e487fb1..a9a24ad90 100644 --- a/sqlspec/adapters/sqlite/driver.py +++ b/sqlspec/adapters/sqlite/driver.py @@ -146,7 +146,7 @@ def dispatch_execute(self, cursor: Any, statement: "SQL") -> "ExecutionResult": cursor, statement.operation_type, effective_rowcount, - statement.expression, + statement._peek_expression(), self._rowid_target_cache, ) @@ -166,7 +166,7 @@ def dispatch_execute(self, cursor: Any, statement: "SQL") -> "ExecutionResult": cursor, statement.operation_type, affected_rows, - statement.expression, + statement._peek_expression(), self._rowid_target_cache, ) return self.create_execution_result(cursor, rowcount_override=affected_rows, last_inserted_id=last_inserted_id) @@ -451,7 +451,6 @@ def _execute_cache_hit( statements while preserving driver exception mapping behavior. """ cursor = None - direct_statement: SQL | None = None returns_rows = cached.operation_profile.returns_rows self._invalidate_rowid_target_cache(cached.operation_type) try: @@ -489,25 +488,20 @@ def _execute_cache_hit( description = cursor.description column_names = [col[0] for col in description] if description else [] row_format = resolve_row_format(fetched_data) - execution_result = self.create_execution_result( - cursor, + return self.build_cached_select_result( + sql, + params, + cached, selected_data=fetched_data, column_names=column_names, data_row_count=len(fetched_data), - is_select_result=True, row_format=row_format, last_inserted_id=last_inserted_id, ) - direct_statement = self._cached_statement( - sql, params, cached, params, params_are_simple=True, compiled_sql=cached.compiled_sql - ) - return self.build_statement_result(direct_statement, execution_result) finally: if cursor is not None: with contextlib.suppress(Exception): cursor.close() - if direct_statement is not None: - self._release_pooled_statement(direct_statement) def _invalidate_rowid_target_cache(self, operation_type: "OperationType") -> None: if operation_type not in {"SELECT", "INSERT", "UPDATE", "DELETE"}: diff --git a/sqlspec/config.py b/sqlspec/config.py index 8e33dfdbd..86cc89238 100644 --- a/sqlspec/config.py +++ b/sqlspec/config.py @@ -28,6 +28,7 @@ create_sync_pool, seed_runtime_driver_features, ) +from sqlspec.driver._query_cache import StatementCacheHolder, resolve_statement_cache_size from sqlspec.exceptions import ImproperConfigurationError, MissingDependencyError from sqlspec.extensions.events import EventRuntimeHints from sqlspec.loader import SQLFileLoader @@ -1351,6 +1352,11 @@ def _prepare_driver(self, driver: DriverT) -> DriverT: """Attach observability runtime to driver instances before returning them.""" driver.attach_observability(self.get_observability_runtime()) + if ( + driver.statement_config is self.statement_config + and self.driver_features.get("_statement_cache_holder") is not driver._cache_holder + ): + self.driver_features["_statement_cache_holder"] = driver._cache_holder return driver @staticmethod @@ -1438,6 +1444,9 @@ def _init_config_state( self.statement_config = statement_config or build_default_statement_config(default_dialect) self._storage_capabilities = None self.driver_features = seed_runtime_driver_features(driver_features, self.storage_capabilities()) + self.driver_features["_statement_cache_holder"] = StatementCacheHolder( + self.statement_config, resolve_statement_cache_size(self.driver_features) + ) self._attach_lifecycle_hooks() self._configure_observability_extensions() diff --git a/sqlspec/core/compiler.py b/sqlspec/core/compiler.py index aedc4a8e1..fddba3b28 100644 --- a/sqlspec/core/compiler.py +++ b/sqlspec/core/compiler.py @@ -503,23 +503,8 @@ def _apply_dynamic_sqlcommenter(self, result: CompiledSQL) -> CompiledSQL: def _materialize_cached_result(self, cached_result: CompiledSQL, parameters: Any, is_many: bool) -> CompiledSQL: """Return cached compilation metadata with parameters for the current call.""" - cloned_expr = cached_result.expression.copy() if cached_result.expression is not None else None if self._parameter_config.needs_static_script_compilation: - if cloned_expr is None: - return cached_result - return CompiledSQL( - compiled_sql=cached_result.compiled_sql, - execution_parameters=cached_result.execution_parameters, - operation_type=cached_result.operation_type, - expression=cloned_expr, - parameter_style=cached_result.parameter_style, - supports_many=cached_result.supports_many, - parameter_casts=cached_result.parameter_casts, - parameter_profile=cached_result.parameter_profile, - operation_profile=cached_result.operation_profile, - input_named_parameters=cached_result.input_named_parameters, - applied_wrap_types=cached_result.applied_wrap_types, - ) + return cached_result processed_params = self._parameter_processor.transform_cached_parameters( parameters, @@ -539,7 +524,7 @@ def _materialize_cached_result(self, cached_result: CompiledSQL, parameters: Any compiled_sql=final_sql, execution_parameters=processed_params, operation_type=cached_result.operation_type, - expression=cloned_expr, + expression=cached_result.expression, parameter_style=cached_result.parameter_style, supports_many=cached_result.supports_many, parameter_casts=cached_result.parameter_casts, @@ -638,11 +623,7 @@ def _unpack_parse_cache_entry( """ cached_expression, cached_operation, cached_profile = parse_cache_entry operation_profile = OperationProfile(returns_rows=cached_profile[0], modifies_rows=cached_profile[1]) - return ( - cached_expression.copy() if cached_expression is not None else None, - cached_operation, - operation_profile, - ) + return (cached_expression, cached_operation, operation_profile) def _resolve_expression( self, sqlglot_sql: str, dialect_str: "str | None", expression_override: "exp.Expr | None" diff --git a/sqlspec/core/parameters/_processor.py b/sqlspec/core/parameters/_processor.py index eedfd26be..e5d5ab58f 100644 --- a/sqlspec/core/parameters/_processor.py +++ b/sqlspec/core/parameters/_processor.py @@ -30,7 +30,7 @@ ) from sqlspec.core.parameters._validator import ParameterValidator, unescape_qmark_operator from sqlspec.exceptions import SQLSpecError -from sqlspec.utils.dispatch import TypeDispatcher +from sqlspec.utils.dispatch import _NO_MATCH, TypeDispatcher _OCCURRENCE_KEYED_STYLE_VALUES: "frozenset[str]" = frozenset({ ParameterStyle.QMARK.value, @@ -50,8 +50,15 @@ _EXECUTE_MANY_SAMPLE_THRESHOLD: Final[int] = 10 _EXECUTE_MANY_SAMPLE_SIZE: Final[int] = 3 +_FALLBACK_CACHE_MAX_SIZE: Final[int] = 256 _TYPE_COERCION_DISPATCHERS: Final[dict[tuple[TypeCoercionFallback, ...], TypeDispatcher[Callable[[Any], Any]]]] = {} +_TYPE_COERCION_DISPATCHERS_BY_ID: Final[ + dict[int, tuple[tuple[TypeCoercionFallback, ...], TypeDispatcher[Callable[[Any], Any]]]] +] = {} +_TYPE_COERCION_FALLBACKS_CACHE: Final[ + dict[int, tuple[dict[type, Callable[[Any], Any]], int, tuple[TypeCoercionFallback, ...]]] +] = {} def structural_fingerprint(parameters: "ParameterPayload", is_many: bool = False) -> Any: @@ -954,13 +961,24 @@ def type_coercion_dispatcher( fallback_items: "tuple[TypeCoercionFallback, ...]", ) -> "TypeDispatcher[Callable[[Any], Any]]": """Return the process-wide dispatcher registered for the fallback items.""" + items_id = id(fallback_items) + cached_by_id = _TYPE_COERCION_DISPATCHERS_BY_ID.get(items_id) + if cached_by_id is not None and cached_by_id[0] is fallback_items: + return cached_by_id[1] + dispatcher = _TYPE_COERCION_DISPATCHERS.get(fallback_items) if dispatcher is not None: + if len(_TYPE_COERCION_DISPATCHERS_BY_ID) >= _FALLBACK_CACHE_MAX_SIZE: + _TYPE_COERCION_DISPATCHERS_BY_ID.clear() + _TYPE_COERCION_DISPATCHERS_BY_ID[items_id] = (fallback_items, dispatcher) return dispatcher dispatcher = TypeDispatcher["Callable[[Any], Any]"]() dispatcher.register_all(fallback_items) _TYPE_COERCION_DISPATCHERS[fallback_items] = dispatcher + if len(_TYPE_COERCION_DISPATCHERS_BY_ID) >= _FALLBACK_CACHE_MAX_SIZE: + _TYPE_COERCION_DISPATCHERS_BY_ID.clear() + _TYPE_COERCION_DISPATCHERS_BY_ID[items_id] = (fallback_items, dispatcher) return dispatcher @@ -970,7 +988,16 @@ def type_coercion_fallbacks( """Return the ordered fallback items for a coercion map, empty when none is configured.""" if not type_coercion_map: return () - return tuple(type_coercion_map.items()) + map_id = id(type_coercion_map) + map_len = len(type_coercion_map) + cached = _TYPE_COERCION_FALLBACKS_CACHE.get(map_id) + if cached is not None and cached[0] is type_coercion_map and cached[1] == map_len: + return cached[2] + if len(_TYPE_COERCION_FALLBACKS_CACHE) >= _FALLBACK_CACHE_MAX_SIZE: + _TYPE_COERCION_FALLBACKS_CACHE.clear() + items = tuple(type_coercion_map.items()) + _TYPE_COERCION_FALLBACKS_CACHE[map_id] = (type_coercion_map, map_len, items) + return items def apply_type_coercion( @@ -1041,9 +1068,22 @@ def _coerce_sequence_if_needed( seq_value: "Sequence[Any]", type_coercion_map: "dict[type, Callable[[Any], Any]]", fallback_items: "tuple[TypeCoercionFallback, ...]", + dispatcher: "TypeDispatcher[Callable[[Any], Any]] | None" = None, ) -> "Sequence[Any] | list[Any]": + if dispatcher is None and fallback_items: + dispatcher = type_coercion_dispatcher(fallback_items) + dispatcher_cache = dispatcher._cache if dispatcher is not None else None updated_seq: list[Any] | None = None for idx, item in enumerate(seq_value): + if updated_seq is None: + if item is None: + continue + item_type = type(item) + if item_type is not TypedParameter and item_type not in type_coercion_map: + if dispatcher_cache is None or dispatcher_cache.get(item_type) is _NO_MATCH: + continue + if dispatcher is not None and dispatcher.resolve_type(item_type) is None: + continue coerced_value = _coerce_parameter_value(item, type_coercion_map, fallback_items) if updated_seq is None: if coerced_value is item: @@ -1059,9 +1099,22 @@ def _coerce_mapping_if_needed( mapping: "Mapping[Any, Any]", type_coercion_map: "dict[type, Callable[[Any], Any]]", fallback_items: "tuple[TypeCoercionFallback, ...]", + dispatcher: "TypeDispatcher[Callable[[Any], Any]] | None" = None, ) -> "Mapping[Any, Any] | dict[Any, Any]": + if dispatcher is None and fallback_items: + dispatcher = type_coercion_dispatcher(fallback_items) + dispatcher_cache = dispatcher._cache if dispatcher is not None else None updated_mapping: dict[Any, Any] | None = None for key, val in mapping.items(): + if updated_mapping is None: + if val is None: + continue + val_type = type(val) + if val_type is not TypedParameter and val_type not in type_coercion_map: + if dispatcher_cache is None or dispatcher_cache.get(val_type) is _NO_MATCH: + continue + if dispatcher is not None and dispatcher.resolve_type(val_type) is None: + continue coerced_value = _coerce_parameter_value(val, type_coercion_map, fallback_items) if updated_mapping is None: if coerced_value is val: @@ -1077,26 +1130,29 @@ def _coerce_parameter_set( param_set: object, type_coercion_map: "dict[type, Callable[[Any], Any]]", fallback_items: "tuple[TypeCoercionFallback, ...]", + dispatcher: "TypeDispatcher[Callable[[Any], Any]] | None" = None, ) -> object: param_type = type(param_set) if param_type is list: - return _coerce_sequence_if_needed(cast("list[Any]", param_set), type_coercion_map, fallback_items) + return _coerce_sequence_if_needed(cast("list[Any]", param_set), type_coercion_map, fallback_items, dispatcher) if param_type is tuple: seq_value = cast("tuple[Any, ...]", param_set) - coerced_seq = _coerce_sequence_if_needed(seq_value, type_coercion_map, fallback_items) + coerced_seq = _coerce_sequence_if_needed(seq_value, type_coercion_map, fallback_items, dispatcher) if coerced_seq is seq_value: return seq_value return tuple(cast("list[Any]", coerced_seq)) if param_type is dict: - return _coerce_mapping_if_needed(cast("dict[Any, Any]", param_set), type_coercion_map, fallback_items) + return _coerce_mapping_if_needed( + cast("dict[Any, Any]", param_set), type_coercion_map, fallback_items, dispatcher + ) if isinstance(param_set, Sequence) and not isinstance(param_set, (str, bytes)): seq_fallback = param_set - coerced_seq = _coerce_sequence_if_needed(seq_fallback, type_coercion_map, fallback_items) + coerced_seq = _coerce_sequence_if_needed(seq_fallback, type_coercion_map, fallback_items, dispatcher) if coerced_seq is seq_fallback: return param_set return coerced_seq if isinstance(param_set, Mapping): - coerced_mapping = _coerce_mapping_if_needed(param_set, type_coercion_map, fallback_items) + coerced_mapping = _coerce_mapping_if_needed(param_set, type_coercion_map, fallback_items, dispatcher) if coerced_mapping is param_set: return param_set return coerced_mapping @@ -1109,13 +1165,14 @@ def _coerce_parameters_payload( fallback_items: "tuple[TypeCoercionFallback, ...]", is_many: bool, ) -> object: + dispatcher = type_coercion_dispatcher(fallback_items) if fallback_items else None param_type = type(parameters) if param_type is list: seq_params = cast("list[Any]", parameters) if is_many: updated_many: list[Any] | None = None for idx, param_set in enumerate(seq_params): - coerced_set = _coerce_parameter_set(param_set, type_coercion_map, fallback_items) + coerced_set = _coerce_parameter_set(param_set, type_coercion_map, fallback_items, dispatcher) if updated_many is None: if coerced_set is param_set: continue @@ -1124,23 +1181,28 @@ def _coerce_parameters_payload( if updated_many is None: return seq_params return updated_many - return _coerce_sequence_if_needed(seq_params, type_coercion_map, fallback_items) + return _coerce_sequence_if_needed(seq_params, type_coercion_map, fallback_items, dispatcher) if param_type is tuple: tuple_params = cast("tuple[Any, ...]", parameters) if is_many: - return [_coerce_parameter_set(param_set, type_coercion_map, fallback_items) for param_set in tuple_params] - coerced_tuple = _coerce_sequence_if_needed(tuple_params, type_coercion_map, fallback_items) + return [ + _coerce_parameter_set(param_set, type_coercion_map, fallback_items, dispatcher) + for param_set in tuple_params + ] + coerced_tuple = _coerce_sequence_if_needed(tuple_params, type_coercion_map, fallback_items, dispatcher) return list(coerced_tuple) if param_type is dict: dict_params = cast("dict[Any, Any]", parameters) - return _coerce_mapping_if_needed(dict_params, type_coercion_map, fallback_items) + return _coerce_mapping_if_needed(dict_params, type_coercion_map, fallback_items, dispatcher) if is_many and isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)): - return [_coerce_parameter_set(param_set, type_coercion_map, fallback_items) for param_set in parameters] + return [ + _coerce_parameter_set(param_set, type_coercion_map, fallback_items, dispatcher) for param_set in parameters + ] if isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)): - coerced_sequence = _coerce_sequence_if_needed(parameters, type_coercion_map, fallback_items) + coerced_sequence = _coerce_sequence_if_needed(parameters, type_coercion_map, fallback_items, dispatcher) return list(coerced_sequence) if isinstance(parameters, Mapping): - coerced_mapping = _coerce_mapping_if_needed(parameters, type_coercion_map, fallback_items) + coerced_mapping = _coerce_mapping_if_needed(parameters, type_coercion_map, fallback_items, dispatcher) return dict(coerced_mapping) return _coerce_parameter_value(parameters, type_coercion_map, fallback_items) diff --git a/sqlspec/core/result/_base.py b/sqlspec/core/result/_base.py index bef1fe69a..fef16e5c4 100644 --- a/sqlspec/core/result/_base.py +++ b/sqlspec/core/result/_base.py @@ -15,7 +15,7 @@ from mypy_extensions import mypyc_attr from typing_extensions import TypeVar -from sqlspec.core.statement import SQL +from sqlspec.core.statement import SQL, ProcessedState from sqlspec.exceptions import MultipleResultsFoundError from sqlspec.storage import ( AsyncStoragePipeline, @@ -44,6 +44,7 @@ from collections.abc import Iterator, Sequence from sqlspec.core.compiler import OperationType + from sqlspec.core.statement import StatementConfig from sqlspec.typing import ArrowReturnFormat, ArrowTable, PandasDataFrame, PolarsDataFrame, SchemaT @@ -75,20 +76,25 @@ class StatementResult: """ __slots__ = ( + "_lazy_cached_state", + "_lazy_compiled_sql", + "_lazy_config", + "_lazy_params", + "_lazy_sql", "_operation_type", + "_statement", "data", "execution_time", "last_inserted_id", "metadata", "rows_affected", - "statement", ) _operation_type: "OperationType" def __init__( self, - statement: "SQL", + statement: "SQL | None" = None, data: Any = None, rows_affected: int = 0, last_inserted_id: int | str | None = None, @@ -105,14 +111,54 @@ def __init__( execution_time: Time taken to execute the statement in seconds. metadata: Additional metadata about the operation. """ - self.statement = statement - self._operation_type = statement.operation_type + self._statement: SQL | None = statement + self._lazy_sql: str | None = None + self._lazy_config: StatementConfig | None = None + self._lazy_cached_state: ProcessedState | None = None + self._lazy_compiled_sql: str | None = None + self._lazy_params: Any = None + self._operation_type = statement.operation_type if statement is not None else "SELECT" self.data = data self.rows_affected = rows_affected self.last_inserted_id = last_inserted_id self.execution_time = execution_time self.metadata = metadata if metadata is not None else {} + @property + def statement(self) -> "SQL": + """Return the executed SQL statement, materializing it on demand for fast-path results.""" + stmt = self._statement + if stmt is not None: + return stmt + raw_sql = self._lazy_sql + config = self._lazy_config + cached_state = self._lazy_cached_state + if raw_sql is not None and config is not None and cached_state is not None: + direct_state = ProcessedState( + compiled_sql=self._lazy_compiled_sql or cached_state.compiled_sql, + execution_parameters=self._lazy_params, + parsed_expression=cached_state.parsed_expression, + operation_type=cached_state.operation_type, + input_named_parameters=cached_state.input_named_parameters, + applied_wrap_types=cached_state.applied_wrap_types, + filter_hash=cached_state.filter_hash, + parameter_fingerprint=cached_state.parameter_fingerprint, + parameter_casts=cached_state.parameter_casts, + validation_errors=cached_state.validation_errors, + parameter_profile=cached_state.parameter_profile, + operation_profile=cached_state.operation_profile, + is_many=False, + expression_is_shared=True, + ) + stmt = SQL._create_cached_direct(raw_sql, config, direct_state, pooled=False) + self._statement = stmt + return stmt + return _EMPTY_RESULT_STATEMENT + + @statement.setter + def statement(self, value: "SQL") -> None: + self._statement = value + @abstractmethod def __iter__(self) -> "Iterator[Any]": """Iterate over result rows.""" @@ -161,6 +207,9 @@ def set_metadata(self, key: str, value: Any) -> None: key: The metadata key to set. value: The value to set. """ + if self.metadata is _DEFAULT_DML_METADATA: + self.metadata = {key: value} + return self.metadata[key] = value @@ -197,7 +246,7 @@ class SQLResult(StatementResult): def __init__( self, - statement: "SQL", + statement: "SQL | None" = None, data: "Sequence[Any] | None" = None, rows_affected: int = 0, last_inserted_id: int | str | None = None, @@ -331,6 +380,39 @@ def num_columns(self) -> int: """ return len(self.column_names) if self.column_names else 0 + @classmethod + def create_cached_select( + cls, + sql: str, + config: "StatementConfig", + cached_state: "ProcessedState", + compiled_sql: str, + params: Any, + data: "list[Any]", + column_names: "list[str]", + rows_affected: int, + row_format: RowFormat = "dict", + last_inserted_id: int | str | None = None, + ) -> "SQLResult": + """Construct a fast-path SELECT result with deferred SQL statement materialization.""" + result = cls( + statement=None, + data=data, + rows_affected=rows_affected, + last_inserted_id=last_inserted_id, + metadata=_DEFAULT_DML_METADATA, + operation_type="SELECT", + column_names=column_names, + total_count=rows_affected, + row_format=row_format, + ) + result._lazy_sql = sql + result._lazy_config = config + result._lazy_cached_state = cached_state + result._lazy_compiled_sql = compiled_sql + result._lazy_params = params + return result + def is_success(self) -> bool: """Check if the operation was successful. @@ -965,7 +1047,12 @@ class DMLResult(SQLResult): def __init__( self, op_type: "OperationType", rows_affected: int = 0, last_inserted_id: int | str | None = None ) -> None: - self.statement = _EMPTY_RESULT_STATEMENT + self._statement = _EMPTY_RESULT_STATEMENT + self._lazy_sql = None + self._lazy_config = None + self._lazy_cached_state = None + self._lazy_compiled_sql = None + self._lazy_params = None self.data = _EMPTY_RESULT_DATA self.rows_affected = rows_affected self.last_inserted_id = last_inserted_id diff --git a/sqlspec/core/statement.py b/sqlspec/core/statement.py index 0e0bba94f..9fbc98501 100644 --- a/sqlspec/core/statement.py +++ b/sqlspec/core/statement.py @@ -117,6 +117,7 @@ PROCESSED_STATE_SLOTS: Final = ( "compiled_sql", "execution_parameters", + "expression_is_shared", "parsed_expression", "operation_type", "input_named_parameters", @@ -178,6 +179,7 @@ def __init__( parameter_profile: "ParameterProfile | None" = None, operation_profile: "OperationProfile | None" = None, is_many: bool = False, + expression_is_shared: bool = False, ) -> None: self.compiled_sql = compiled_sql self.execution_parameters = execution_parameters @@ -192,6 +194,7 @@ def __init__( self.parameter_profile = parameter_profile or ParameterProfile.empty() self.operation_profile = operation_profile or OperationProfile.empty() self.is_many = is_many + self.expression_is_shared = expression_is_shared def __hash__(self) -> int: return hash((self.compiled_sql, str(self.execution_parameters), self.operation_type)) @@ -214,9 +217,19 @@ def __reduce__(self) -> "tuple[Any, ...]": self.parameter_profile, self.operation_profile, self.is_many, + self.expression_is_shared, ), ) + def ensure_owned_expression(self) -> "exp.Expr | None": + """Return an owned SQLGlot expression, copying once if currently shared with a cache.""" + expr = self.parsed_expression + if expr is not None and self.expression_is_shared: + expr = expr.copy() + self.parsed_expression = expr + self.expression_is_shared = False + return expr + def reset(self) -> None: """Reset processing state for reuse.""" self.compiled_sql = "" @@ -232,6 +245,7 @@ def reset(self) -> None: self.parameter_profile = ParameterProfile.empty() self.operation_profile = OperationProfile.empty() self.is_many = False + self.expression_is_shared = False @mypyc_attr(allow_interpreted_subclasses=False) @@ -388,7 +402,17 @@ def parameters(self) -> Any: return mixed[1] if self._named_parameters: return self._named_parameters - return self._positional_parameters or [] + if self._positional_parameters: + return self._positional_parameters + if self._is_cache_direct and self._processed_state is not Empty: + exec_params = self._processed_state.execution_parameters + if isinstance(exec_params, dict): + return exec_params + if isinstance(exec_params, tuple): + return list(exec_params) + if isinstance(exec_params, list): + return exec_params + return [] @property def positional_parameters(self) -> "list[Any]": @@ -431,7 +455,7 @@ def expression(self) -> "exp.Expr | None": """ state = self._processed_state if state is not Empty: - return state.parsed_expression + return state.ensure_owned_expression() return self._raw_expression @property @@ -466,7 +490,7 @@ def statement_expression(self) -> "exp.Expr | None": """ state = self._processed_state if state is not Empty: - return state.parsed_expression + return state.ensure_owned_expression() return self._raw_expression @property @@ -629,6 +653,7 @@ def compile(self) -> "tuple[str, Any]": operation_profile=compiled_result.operation_profile, validation_errors=[], is_many=self._is_many, + expression_is_shared=compiled_result.expression is not None, ) except sqlspec.exceptions.SQLSpecError: raise @@ -1139,7 +1164,9 @@ def builder(self, dialect: "DialectType | None" = None) -> "QueryBuilder": return builder @classmethod - def _create_cached_direct(cls, sql: str, config: "StatementConfig", processed_state: "ProcessedState") -> "SQL": + def _create_cached_direct( + cls, sql: str, config: "StatementConfig", processed_state: "ProcessedState", *, pooled: bool = True + ) -> "SQL": """Create a minimal SQL object for direct (fast-path) execution. Bypasses standard __init__ and parameter processing. @@ -1150,7 +1177,7 @@ def _create_cached_direct(cls, sql: str, config: "StatementConfig", processed_st stmt._statement_config = config stmt._dialect = stmt._normalize_dialect(config.dialect) stmt._processed_state = processed_state - stmt._pooled = True + stmt._pooled = pooled stmt._is_cache_direct = True stmt._is_many = False stmt._is_script = False @@ -1360,11 +1387,7 @@ def _rebind_cached_parameters(self, state: "ProcessedState") -> "tuple[str, Any] mixed = self._resolve_mixed_parameter_inputs() params = mixed[1] if mixed is not None else self._named_parameters or self._positional_parameters if self._rebind_processor is None: - self._rebind_processor = ParameterProcessor( - converter=self._statement_config.parameter_converter, - validator=self._statement_config.parameter_validator, - cache_max_size=0, - ) + self._rebind_processor = _get_rebind_processor(self._statement_config) processor = self._rebind_processor rebound_params = processor.transform_cached_parameters( params, @@ -1378,10 +1401,12 @@ def _rebind_cached_parameters(self, state: "ProcessedState") -> "tuple[str, Any] output_transformer = self._statement_config.output_transformer if output_transformer: compiled_sql, rebound_params = output_transformer(compiled_sql, rebound_params) + if state.parsed_expression is not None: + state.expression_is_shared = True self._processed_state = self._build_processed_state( compiled_sql=compiled_sql, execution_parameters=rebound_params, - parsed_expression=state.parsed_expression.copy() if state.parsed_expression is not None else None, + parsed_expression=state.parsed_expression, operation_type=state.operation_type, input_named_parameters=state.input_named_parameters, applied_wrap_types=state.applied_wrap_types, @@ -1392,6 +1417,7 @@ def _rebind_cached_parameters(self, state: "ProcessedState") -> "tuple[str, Any] operation_profile=state.operation_profile, validation_errors=state.validation_errors.copy(), is_many=state.is_many, + expression_is_shared=state.parsed_expression is not None, ) self._compiled_from_cache = False return compiled_sql, rebound_params @@ -1423,15 +1449,21 @@ def _create_empty_copy(self) -> "SQL": new_sql._pooled = True source_state = self._processed_state - is_processed = source_state is not Empty - new_sql._compiled_from_cache = is_processed - new_sql._processed_state = source_state if is_processed else Empty + if source_state is not Empty: + if source_state.parsed_expression is not None: + source_state.expression_is_shared = True + new_sql._compiled_from_cache = True + new_sql._processed_state = source_state + else: + new_sql._compiled_from_cache = False + new_sql._processed_state = Empty new_sql._hash = None new_sql._filters = self._filters.copy() new_sql._named_parameters = {} new_sql._positional_parameters = [] new_sql._sql_param_counters = self._sql_param_counters.copy() new_sql._declared_parameters = self._declared_parameters + new_sql._rebind_processor = self._rebind_processor return new_sql @@ -1451,6 +1483,7 @@ def _build_processed_state( operation_profile: "OperationProfile | None", validation_errors: "list[str] | None", is_many: bool, + expression_is_shared: bool = False, ) -> "ProcessedState": state = get_processed_state_pool().acquire() ProcessedState.__init__( @@ -1468,6 +1501,7 @@ def _build_processed_state( parameter_profile=parameter_profile, operation_profile=operation_profile, is_many=is_many, + expression_is_shared=expression_is_shared, ) return state @@ -1522,6 +1556,13 @@ def _next_parameter_name(self, base_name: str) -> str: self._sql_param_counters[prefixed_base] = next_index return candidate + def _peek_expression(self) -> "exp.Expr | None": + """Return the parsed or raw SQLGlot expression for read-only inspection without copying.""" + state = self._processed_state + if state is not Empty: + return state.parsed_expression + return self._raw_expression + def _current_expression(self) -> exp.Expr: """Get the current expression or parse the raw SQL. @@ -1936,6 +1977,8 @@ def _compare_parameter_configs(self, config1: Any, config2: Any) -> bool: _DEFAULT_CONFIG: "StatementConfig | None" = None +_REBIND_PROCESSORS: "dict[tuple[int, int], tuple[Any, Any, ParameterProcessor]]" = {} +_REBIND_PROCESSOR_CACHE_MAX_SIZE: Final[int] = 256 def get_default_config() -> StatementConfig: @@ -1962,6 +2005,9 @@ def get_default_parameter_config() -> ParameterStyleConfig: ) +Statement: TypeAlias = str | exp.Expr | SQL + + def _rebuild_sql( raw_sql: str, original_parameters: "tuple[Any, ...]", @@ -1989,7 +2035,19 @@ def _rebuild_sql( return new_sql -Statement: TypeAlias = str | exp.Expr | SQL +def _get_rebind_processor(config: "StatementConfig") -> ParameterProcessor: + """Return a shared stateless ParameterProcessor for cached-statement rebinding.""" + converter = config.parameter_converter + validator = config.parameter_validator + key = (id(converter), id(validator)) + cached = _REBIND_PROCESSORS.get(key) + if cached is not None and cached[0] is converter and cached[1] is validator: + return cached[2] + if len(_REBIND_PROCESSORS) >= _REBIND_PROCESSOR_CACHE_MAX_SIZE: + _REBIND_PROCESSORS.clear() + processor = ParameterProcessor(converter=converter, validator=validator, cache_max_size=0) + _REBIND_PROCESSORS[key] = (converter, validator, processor) + return processor def _parse_order_item(order_item: str, dialect: "str | None", enable_parsing: bool) -> exp.Expr: diff --git a/sqlspec/driver/_async.py b/sqlspec/driver/_async.py index fc1dcb4d0..30516b8b6 100644 --- a/sqlspec/driver/_async.py +++ b/sqlspec/driver/_async.py @@ -1,6 +1,7 @@ """Asynchronous driver protocol implementation.""" import logging +import sys from abc import abstractmethod from inspect import isawaitable from time import perf_counter @@ -30,6 +31,7 @@ ExecutionResult, StackExecutionObserver, _raise_database_exception, + _resolve_fast_params, describe_stack_statement, handle_single_row_error, validate_savepoint_name, @@ -55,7 +57,7 @@ from sqlglot.dialects.dialect import DialectType from sqlspec.builder import QueryBuilder - from sqlspec.core import ArrowResult, SQLResult, Statement, StatementConfig, StatementFilter + from sqlspec.core import ArrowResult, OperationType, SQLResult, Statement, StatementConfig, StatementFilter from sqlspec.data_dictionary import ( ColumnMetadata, ForeignKeyMetadata, @@ -247,82 +249,34 @@ async def dispatch_statement_execution(self, statement: "SQL", connection: "Any" if runtime is None or runtime.is_idle: exc_handler = self.handle_database_exceptions() - result = await self._run_with_exception_handler( - exc_handler, - self._dispatch_statement_with_cursor, - connection, - statement, - has_execution_parameters=bool(execution_parameters), - ) + if sys.exc_info()[1] is None: + async with exc_handler: + result = await self._dispatch_statement_with_cursor( + connection, statement, has_execution_parameters=bool(execution_parameters) + ) + else: + result = await self._run_with_exception_handler( + exc_handler, + self._dispatch_statement_with_cursor, + connection, + statement, + has_execution_parameters=bool(execution_parameters), + ) self._check_pending_exception(exc_handler) assert result is not None return result - operation = statement.operation_type - query_context = { - "sql": compiled_sql, - "parameters": execution_parameters, - "driver": type(self).__name__, - "operation": operation, - "is_many": statement.is_many, - "is_script": statement.is_script, - } - await runtime.emit_query_start_async(**query_context) - sql_hash = None - if runtime.span_manager.is_enabled or ( - runtime.has_statement_observers and runtime.config.logging and runtime.config.logging.include_sql_hash - ): - sql_hash = observability_runtime.compute_sql_hash(compiled_sql) - span = runtime.start_query_span(compiled_sql, operation, type(self).__name__, sql_hash=sql_hash) - started = perf_counter() - - exc_handler = self.handle_database_exceptions() - try: - result = await self._run_with_exception_handler( - exc_handler, - self._dispatch_statement_with_cursor, - connection, - statement, - has_execution_parameters=bool(execution_parameters), - ) - except Exception as exc: # pragma: no cover - pending_exception = exc_handler.pending_exception - if pending_exception is not None: - mapped_exc = pending_exception - runtime.span_manager.end_span(span, error=mapped_exc) - await runtime.emit_error_async(mapped_exc, **query_context) - _raise_database_exception(exc_handler, exc) - runtime.span_manager.end_span(span, error=exc) - await runtime.emit_error_async(exc, **query_context) - _raise_database_exception(exc_handler, exc) - - pending_exception = exc_handler.pending_exception - if pending_exception is not None: - mapped_exc = pending_exception - runtime.span_manager.end_span(span, error=mapped_exc) - await runtime.emit_error_async(mapped_exc, **query_context) - _raise_database_exception(exc_handler, None) - - assert result is not None - - runtime.span_manager.end_span(span) - duration = perf_counter() - started - await runtime.emit_query_complete_async(**{**query_context, "rows_affected": result.rows_affected}) - runtime.emit_statement_event( - sql=compiled_sql, - parameters=execution_parameters, - driver=type(self).__name__, - operation=operation, - execution_mode=self.statement_config.execution_mode, + return await self._execute_with_observability_async( + compiled_sql=compiled_sql, + execution_parameters=execution_parameters, + operation=statement.operation_type, is_many=statement.is_many, is_script=statement.is_script, - rows_affected=result.rows_affected, - duration_s=duration, - storage_backend=(result.metadata or {}).get("storage_backend"), - started_at=started, - precomputed_sql_hash=sql_hash, + runner=self._dispatch_statement_observed_async, + connection=connection, + statement=statement, + has_execution_parameters=bool(execution_parameters), ) - return result finally: self._release_pooled_statement(statement) @@ -1524,6 +1478,90 @@ async def _run_with_exception_handler( """Run an async operation without inheriting the caller's active exception state.""" return await _run_with_async_exception_handler(exc_handler, operation, *args, **kwargs) + async def _execute_with_observability_async( + self, + *, + compiled_sql: str, + execution_parameters: Any, + operation: "OperationType", + is_many: bool, + is_script: bool, + runner: "Callable[..., Awaitable[SQLResult]]", + **runner_kwargs: Any, + ) -> "SQLResult": + """Execute an async runner while emitting spans, lifecycle hooks, and statement events.""" + runtime = self._observability + if runtime is None or runtime.is_idle: + return await runner(**runner_kwargs) + driver_name = type(self).__name__ + query_context = { + "sql": compiled_sql, + "parameters": execution_parameters, + "driver": driver_name, + "operation": operation, + "is_many": is_many, + "is_script": is_script, + } + await runtime.emit_query_start_async(**query_context) + sql_hash = None + if runtime.span_manager.is_enabled or ( + runtime.has_statement_observers and runtime.config.logging and runtime.config.logging.include_sql_hash + ): + sql_hash = observability_runtime.compute_sql_hash(compiled_sql) + span = runtime.start_query_span(compiled_sql, operation, driver_name, sql_hash=sql_hash) + started = perf_counter() + try: + result = await runner(**runner_kwargs) + except Exception as exc: + runtime.span_manager.end_span(span, error=exc) + await runtime.emit_error_async(exc, **query_context) + raise + runtime.span_manager.end_span(span) + duration = perf_counter() - started + await runtime.emit_query_complete_async(**{**query_context, "rows_affected": result.rows_affected}) + runtime.emit_statement_event( + sql=compiled_sql, + parameters=execution_parameters, + driver=driver_name, + operation=operation, + execution_mode=self.statement_config.execution_mode, + is_many=is_many, + is_script=is_script, + rows_affected=result.rows_affected, + duration_s=duration, + storage_backend=(result.metadata or {}).get("storage_backend"), + started_at=started, + precomputed_sql_hash=sql_hash, + ) + return result + + async def _dispatch_statement_observed_async( + self, connection: Any, statement: "SQL", *, has_execution_parameters: bool + ) -> "SQLResult": + """Execute an async statement inside an exception handler and raise any mapped error.""" + exc_handler = self.handle_database_exceptions() + result: SQLResult | None = None + try: + if sys.exc_info()[1] is None: + async with exc_handler: + result = await self._dispatch_statement_with_cursor( + connection, statement, has_execution_parameters=has_execution_parameters + ) + else: + result = await self._run_with_exception_handler( + exc_handler, + self._dispatch_statement_with_cursor, + connection, + statement, + has_execution_parameters=has_execution_parameters, + ) + except Exception as exc: # pragma: no cover + _raise_database_exception(exc_handler, exc) + if exc_handler.pending_exception is not None: + _raise_database_exception(exc_handler, None) + assert result is not None + return result + async def _execute_cache_hit( self, sql: str, params: "tuple[Any, ...] | list[Any] | dict[str, Any]", cached: CachedQuery ) -> "SQLResult": @@ -1544,13 +1582,34 @@ async def _execute_cache_hit( """ exc_handler = self.handle_database_exceptions() result: SQLResult | None = None - result = await self._run_with_exception_handler( - exc_handler, self._execute_cache_hit_with_cursor, sql, params, cached - ) + if sys.exc_info()[1] is None: + async with exc_handler: + result = await self._execute_cache_hit_with_cursor(sql, params, cached) + else: + result = await self._run_with_exception_handler( + exc_handler, self._execute_cache_hit_with_cursor, sql, params, cached + ) self._check_pending_exception(exc_handler) assert result is not None return result + async def _execute_cache_hit_observed( + self, sql: str, params: "tuple[Any, ...] | list[Any] | dict[str, Any]", cached: CachedQuery + ) -> "SQLResult": + """Execute an async fast-path cache hit wrapped in observability instrumentation.""" + obs_params = list(params) if isinstance(params, tuple) else params + return await self._execute_with_observability_async( + compiled_sql=cached.compiled_sql, + execution_parameters=obs_params, + operation=cached.operation_type, + is_many=False, + is_script=False, + runner=self._execute_cache_hit, + sql=sql, + params=params, + cached=cached, + ) + async def _cached_execution( self, statement: str, params: "tuple[Any, ...] | list[Any] | dict[str, Any]" ) -> "SQLResult | None": @@ -1577,9 +1636,13 @@ async def _execute_cached_statement(self, statement: "SQL") -> "SQLResult": exc_handler = self.handle_database_exceptions() result: SQLResult | None = None try: - result = await self._run_with_exception_handler( - exc_handler, self._execute_cached_statement_with_cursor, statement - ) + if sys.exc_info()[1] is None: + async with exc_handler: + result = await self._execute_cached_statement_with_cursor(statement) + else: + result = await self._run_with_exception_handler( + exc_handler, self._execute_cached_statement_with_cursor, statement + ) self._check_pending_exception(exc_handler) assert result is not None @@ -1587,6 +1650,21 @@ async def _execute_cached_statement(self, statement: "SQL") -> "SQLResult": finally: self._release_pooled_statement(statement) + async def _execute_cached_statement_observed( + self, statement: "SQL", compiled_sql: str, execution_parameters: Any, operation: "OperationType" + ) -> "SQLResult": + """Execute a rebound cached async statement wrapped in observability instrumentation.""" + obs_params = list(execution_parameters) if isinstance(execution_parameters, tuple) else execution_parameters + return await self._execute_with_observability_async( + compiled_sql=compiled_sql, + execution_parameters=obs_params, + operation=operation, + is_many=False, + is_script=False, + runner=self._execute_cached_statement, + statement=statement, + ) + async def _execute( self, statement: "SQL | Statement | QueryBuilder", @@ -1594,11 +1672,7 @@ async def _execute( statement_config: "StatementConfig | None", kwargs: "dict[str, Any]", ) -> "SQLResult": - fast_params: tuple[Any, ...] | list[Any] | dict[str, Any] | None = None - if not kwargs and len(parameters) == 1 and isinstance(parameters[0], (tuple, list, dict)): - fast_params = cast("tuple[Any, ...] | list[Any] | dict[str, Any]", parameters[0]) - elif not parameters and kwargs: - fast_params = kwargs + fast_params = _resolve_fast_params(parameters, kwargs) if ( self._stmt_cache_enabled and (statement_config is None or statement_config is self.statement_config) @@ -1705,35 +1779,32 @@ async def _execute_cache_hit_with_cursor( result: SQLResult | None = None try: async with self.with_cursor(self.connection) as cursor: - execute = getattr(cursor, "execute", None) - fetchall = getattr(cursor, "fetchall", None) + returns_rows = cached.operation_profile.returns_rows + execute = getattr(cursor, "execute", None) if cursor is not self.connection else None + fetchall = getattr(cursor, "fetchall", None) if execute is not None and returns_rows else None can_use_cursor_fast_path = execute is not None and ( - (cached.operation_profile.returns_rows and fetchall is not None) - or (not cached.operation_profile.returns_rows and hasattr(cursor, "rowcount")) + (returns_rows and fetchall is not None) or (not returns_rows and hasattr(cursor, "rowcount")) ) if can_use_cursor_fast_path: assert execute is not None - execute_result = execute(cached.compiled_sql, params) + execute_result = execute(cached.compiled_sql, params) if params else execute(cached.compiled_sql) if isawaitable(execute_result): await execute_result - if cached.operation_profile.returns_rows: + if returns_rows: assert fetchall is not None fetched_data = fetchall() if isawaitable(fetched_data): fetched_data = await fetched_data data, column_names, row_count = self.collect_rows(cursor, fetched_data) - execution_result = self.create_execution_result( - cursor, + result = self.build_cached_select_result( + sql, + params, + cached, selected_data=data, column_names=column_names, data_row_count=row_count, - is_select_result=True, row_format=resolve_row_format(data), ) - direct_statement = self._cached_statement( - sql, params, cached, params, params_are_simple=True, compiled_sql=cached.compiled_sql - ) - result = self.build_statement_result(direct_statement, execution_result) else: affected_rows = self.resolve_rowcount(cursor) result = DMLResult(cached.operation_type, affected_rows) @@ -1742,7 +1813,7 @@ async def _execute_cache_hit_with_cursor( sql, params, cached, params, params_are_simple=True, compiled_sql=cached.compiled_sql ) execution_result = await self.dispatch_execute(cursor, direct_statement) - if cached.operation_profile.returns_rows: + if returns_rows: result = self.build_statement_result(direct_statement, execution_result) else: affected_rows = ( diff --git a/sqlspec/driver/_common.py b/sqlspec/driver/_common.py index 14149b3cb..c54cbd1db 100644 --- a/sqlspec/driver/_common.py +++ b/sqlspec/driver/_common.py @@ -3,7 +3,6 @@ import hashlib import logging import re -from collections import OrderedDict from collections.abc import Mapping from time import perf_counter from typing import TYPE_CHECKING, Any, ClassVar, Final, Literal, NamedTuple, NoReturn, Protocol, Self, cast, overload @@ -50,7 +49,7 @@ sort_dependencies, ) from sqlspec.data_dictionary._registry import get_dialect_config -from sqlspec.driver._query_cache import STMT_CACHE_MAX_SIZE, CachedQuery, QueryCache +from sqlspec.driver._query_cache import CachedQuery, QueryCache, StatementCacheHolder, resolve_statement_cache_size from sqlspec.driver._storage_helpers import ( CAPABILITY_HINTS, arrow_table_needs_parameter_preparation, @@ -84,11 +83,11 @@ ) if TYPE_CHECKING: - from collections import abc + from collections import OrderedDict, abc from collections.abc import Awaitable from types import TracebackType - from sqlspec.core import ArrowResult, FilterTypeT, StatementFilter + from sqlspec.core import ArrowResult, FilterTypeT, OperationType, StatementFilter from sqlspec.core.parameters._types import ConvertedParameters from sqlspec.core.stack import StatementStack from sqlspec.data_dictionary._types import DialectConfig @@ -543,6 +542,7 @@ class CommonDriverAttributesMixin: """Common attributes and methods for driver adapters.""" __slots__ = ( + "_cache_holder", "_observability", "_processed_state_pool", "_statement_cache", @@ -583,15 +583,19 @@ def __init__( self.driver_features = driver_features or {} self._observability = observability self._transaction_depth = 0 - self._statement_cache: OrderedDict[str, SQL] = OrderedDict() self._stmt_cache_max_size = self._statement_cache_size() - self._stmt_cache = QueryCache(self._stmt_cache_max_size) - self._stmt_cache_rebind_processor = ParameterProcessor( - converter=self.statement_config.parameter_converter, - validator=self.statement_config.parameter_validator, - cache_max_size=0, - validator_cache_max_size=0, - ) + shared_holder = self.driver_features.get("_statement_cache_holder") + if ( + isinstance(shared_holder, StatementCacheHolder) + and shared_holder.statement_config is self.statement_config + and shared_holder.max_size == self._stmt_cache_max_size + ): + self._cache_holder = shared_holder + else: + self._cache_holder = StatementCacheHolder(self.statement_config, self._stmt_cache_max_size) + self._statement_cache: OrderedDict[str, SQL] = self._cache_holder._statement_cache + self._stmt_cache: QueryCache = self._cache_holder.query_cache + self._stmt_cache_rebind_processor: ParameterProcessor = self._cache_holder.rebind_processor self._stmt_cache_enabled = False self._statement_pool = get_sql_pool() self._processed_state_pool = get_processed_state_pool() @@ -643,12 +647,11 @@ def stmt_cache_rebind( ) -> "ConvertedParameters": """Rebind parameters for a cached query.""" config = self.statement_config.parameter_config - needs_style_remap = bool(_CACHED_NAMED_STYLES.intersection(cached.parameter_profile.styles)) if ( not cached.input_named_parameters and not cached.applied_wrap_types and not config.type_coercion_map - and not needs_style_remap + and not cached.needs_style_remap ): return params return self._stmt_cache_rebind_processor.transform_cached_parameters( @@ -826,9 +829,8 @@ def prepare_statement( statement_config = self.statement_config if isinstance(statement, str): - cached_sql = self._statement_cache.get(statement) + cached_sql = self._cache_holder.get_statement(statement) if cached_sql is not None and not kwargs: - self._statement_cache.move_to_end(statement) has_filters = any(is_statement_filter(p) for p in parameters) if not has_filters: return cached_sql.copy(parameters=parameters) @@ -846,9 +848,7 @@ def prepare_statement( else: sql_statement = self._prepare_from_string(statement, data_parameters, statement_config, kwargs) if not filters and not kwargs and isinstance(statement, str) and self._stmt_cache_max_size > 0: - if len(self._statement_cache) >= self._stmt_cache_max_size: - self._statement_cache.popitem(last=False) - self._statement_cache[statement] = sql_statement + self._cache_holder.set_statement(statement, sql_statement, max_size=self._stmt_cache_max_size) _check_declared_parameters(sql_statement) return self._apply_filters(sql_statement, filters) @@ -958,20 +958,38 @@ def find_filter( """ return _find_filter_impl(filter_type, filters) + def build_cached_select_result( + self, + sql: str, + params: "tuple[Any, ...] | list[Any] | dict[str, Any]", + cached: "CachedQuery", + *, + selected_data: "list[Any]", + column_names: "list[str]", + data_row_count: int, + row_format: "RowFormat" = "dict", + last_inserted_id: int | str | None = None, + ) -> "SQLResult": + """Build a fast-path SELECT result with deferred statement materialization.""" + return SQLResult.create_cached_select( + sql=sql, + config=self.statement_config, + cached_state=cached.processed_state, + compiled_sql=cached.compiled_sql, + params=params, + data=selected_data, + column_names=column_names, + rows_affected=data_row_count, + row_format=row_format, + last_inserted_id=last_inserted_id, + ) + def _statement_cache_size(self) -> int: """Return the configured statement-cache capacity.""" - cache_size = self.driver_features.get("sqlspec_statement_cache_size", STMT_CACHE_MAX_SIZE) - try: - resolved_size = int(cache_size) - except (TypeError, ValueError): - return STMT_CACHE_MAX_SIZE - return max(0, resolved_size) + return resolve_statement_cache_size(self.driver_features) def _refresh_statement_cache_state(self) -> None: - is_observability_idle = self._observability is None or self._observability.is_idle - self._stmt_cache_enabled = bool( - self._stmt_cache_max_size > 0 and not self.statement_config._has_transformers and is_observability_idle - ) + self._stmt_cache_enabled = bool(self._stmt_cache_max_size > 0 and not self.statement_config._has_transformers) def _require_capability(self, capability_flag: str) -> None: """Check that a storage capability is enabled. @@ -1025,9 +1043,7 @@ def _cached_statement( ) effective_compiled_sql = cached.compiled_sql if compiled_sql is None else compiled_sql cached_state = cached.processed_state - direct_state = self._processed_state_pool.acquire() - ProcessedState.__init__( - direct_state, + direct_state = ProcessedState( compiled_sql=effective_compiled_sql, execution_parameters=prepared_parameters, parsed_expression=cached_state.parsed_expression, @@ -1036,13 +1052,14 @@ def _cached_statement( applied_wrap_types=cached_state.applied_wrap_types, filter_hash=cached_state.filter_hash, parameter_fingerprint=cached_state.parameter_fingerprint, - parameter_casts=dict(cached_state.parameter_casts), - validation_errors=cached_state.validation_errors.copy(), + parameter_casts=cached_state.parameter_casts, + validation_errors=cached_state.validation_errors, parameter_profile=cached_state.parameter_profile, operation_profile=cached_state.operation_profile, is_many=False, + expression_is_shared=True, ) - return SQL._create_cached_direct(sql, self.statement_config, direct_state) + return SQL._create_cached_direct(sql, self.statement_config, direct_state, pooled=False) def _sub_statement(self, sql: str, execution_parameters: "ConvertedParameters") -> "SQL": direct_state = self._processed_state_pool.acquire() @@ -1068,21 +1085,25 @@ def _cached_execution( if cached is None or cached.param_count != len(params): return None - param_values = _cache_param_values(params) - config = self.statement_config param_config = config.parameter_config - if param_config.ast_transformer is not None and any(p is None for p in param_values): - return None + if param_config.ast_transformer is not None: + param_values = _cache_param_values(params) + if any(p is None for p in param_values): + return None + else: + param_values = None - needs_rebind = bool(cached.input_named_parameters or cached.applied_wrap_types) - if not needs_rebind: + needs_rebind = bool(cached.input_named_parameters or cached.applied_wrap_types or cached.needs_style_remap) + if not needs_rebind and params: + if param_values is None: + param_values = _cache_param_values(params) needs_rebind = parameter_values_need_processing(param_values, param_config.type_coercion_map) - if not needs_rebind and _CACHED_NAMED_STYLES.intersection(cached.parameter_profile.styles): - needs_rebind = True - if not needs_rebind and not config._has_output_transformer: + runtime = self._observability + if runtime is not None and not runtime.is_idle: + return self._execute_cache_hit_observed(statement, params, cached) return self._execute_cache_hit(statement, params, cached) if needs_rebind: @@ -1101,11 +1122,21 @@ def _cached_execution( prepared = self._cached_statement( statement, params, cached, rebound_params, params_are_simple=params_are_simple, compiled_sql=compiled_sql ) + runtime = self._observability + if runtime is not None and not runtime.is_idle: + return self._execute_cached_statement_observed( + prepared, compiled_sql, rebound_params, cached.operation_type + ) return self._execute_cached_statement(prepared) def _execute_cached_statement(self, statement: "SQL") -> "SQLResult | Awaitable[SQLResult]": raise NotImplementedError + def _execute_cached_statement_observed( + self, statement: "SQL", compiled_sql: str, execution_parameters: Any, operation: "OperationType" + ) -> "SQLResult | Awaitable[SQLResult]": + return self._execute_cached_statement(statement) + def _execute_cache_hit( self, sql: str, params: "tuple[Any, ...] | list[Any] | dict[str, Any]", cached: "CachedQuery" ) -> "SQLResult | Awaitable[SQLResult]": @@ -1120,6 +1151,11 @@ def _execute_cache_hit( ) return self._execute_cached_statement(prepared) + def _execute_cache_hit_observed( + self, sql: str, params: "tuple[Any, ...] | list[Any] | dict[str, Any]", cached: "CachedQuery" + ) -> "SQLResult | Awaitable[SQLResult]": + return self._execute_cache_hit(sql, params, cached) + def _cache_statement(self, statement: "SQL") -> None: """Store statement in cache if eligible. @@ -1159,6 +1195,8 @@ def _cache_statement(self, statement: "SQL") -> None: return processed = cast("ProcessedState", statement.get_processed_state()) + if processed.parsed_expression is None or processed.operation_type in {"COMMAND", "UNKNOWN"}: + return param_profile = processed.parameter_profile cached_state = _clone_processed_state(processed) cached = CachedQuery( @@ -1171,6 +1209,7 @@ def _cache_statement(self, statement: "SQL") -> None: operation_profile=processed.operation_profile, param_count=param_profile.total_count, processed_state=cached_state, + needs_style_remap=bool(_CACHED_NAMED_STYLES.intersection(param_profile.styles)), ) self._stmt_cache.set(statement.raw_sql, cached) @@ -1465,14 +1504,24 @@ def _compiled_statement( if statement.is_processed: if statement._compiled_from_cache: compiled_sql, execution_parameters = statement.compile() - prepared_parameters = self.prepare_driver_parameters( - execution_parameters, statement_config, is_many=statement.is_many, prepared_statement=statement - ) processed = statement.get_processed_state() + if ( + statement.is_many + and isinstance(execution_parameters, list) + and type(self).prepare_driver_parameters is CommonDriverAttributesMixin.prepare_driver_parameters + and _many_parameters_already_prepared( + execution_parameters, statement_config, processed.applied_wrap_types + ) + ): + prepared_parameters: Any = execution_parameters + else: + prepared_parameters = self.prepare_driver_parameters( + execution_parameters, statement_config, is_many=statement.is_many, prepared_statement=statement + ) cached_statement = CachedStatement( compiled_sql=compiled_sql, parameters=cast("tuple[Any, ...] | list[Any] | dict[str, Any] | None", prepared_parameters), - expression=statement.expression, + expression=statement._peek_expression(), input_named_parameters=processed.input_named_parameters, parameter_profile=processed.parameter_profile, applied_wrap_types=processed.applied_wrap_types, @@ -1493,12 +1542,22 @@ def _compiled_statement( ) return cached_statement, prepared_parameters - prepared_parameters = self.prepare_driver_parameters( - processed.execution_parameters, - statement_config, - is_many=statement.is_many, - prepared_statement=statement, - ) + if ( + statement.is_many + and isinstance(processed.execution_parameters, list) + and type(self).prepare_driver_parameters is CommonDriverAttributesMixin.prepare_driver_parameters + and _many_parameters_already_prepared( + processed.execution_parameters, statement_config, processed.applied_wrap_types + ) + ): + prepared_parameters = processed.execution_parameters + else: + prepared_parameters = self.prepare_driver_parameters( + processed.execution_parameters, + statement_config, + is_many=statement.is_many, + prepared_statement=statement, + ) cached_statement = CachedStatement( compiled_sql=processed.compiled_sql, parameters=cast("tuple[Any, ...] | list[Any] | dict[str, Any] | None", prepared_parameters), @@ -1561,15 +1620,23 @@ def _compiled_statement( compiled_sql, execution_parameters = statement.compile() processed = statement.get_processed_state() - prepared_parameters = self.prepare_driver_parameters( - execution_parameters, statement_config, is_many=statement.is_many, prepared_statement=statement - ) + if ( + statement.is_many + and isinstance(execution_parameters, list) + and type(self).prepare_driver_parameters is CommonDriverAttributesMixin.prepare_driver_parameters + and _many_parameters_already_prepared(execution_parameters, statement_config, processed.applied_wrap_types) + ): + prepared_parameters = execution_parameters + else: + prepared_parameters = self.prepare_driver_parameters( + execution_parameters, statement_config, is_many=statement.is_many, prepared_statement=statement + ) cached_parameters = tuple(prepared_parameters) if isinstance(prepared_parameters, list) else prepared_parameters cached_statement = CachedStatement( compiled_sql=compiled_sql, parameters=cast("tuple[Any, ...] | list[Any] | dict[str, Any] | None", cached_parameters), - expression=statement.expression, + expression=statement._peek_expression(), input_named_parameters=processed.input_named_parameters, parameter_profile=processed.parameter_profile, applied_wrap_types=processed.applied_wrap_types, @@ -1865,11 +1932,22 @@ def parameter_values_need_processing( type_coercion_map: "dict[type, Any] | None", fallback_items: "tuple[tuple[type, Any], ...] | None" = None, ) -> bool: + if not values: + return False + if not type_coercion_map: + if len(values) == 1: + return type(values[0]) is TypedParameter + return any(type(value) is TypedParameter for value in values) if fallback_items is None: fallback_items = type_coercion_fallbacks(type_coercion_map) - if len(values) == 1: - return parameter_value_needs_processing(values[0], type_coercion_map, fallback_items) - return any(parameter_value_needs_processing(value, type_coercion_map, fallback_items) for value in values) + dispatcher = type_coercion_dispatcher(fallback_items) + for value in values: + value_type = type(value) + if value_type is TypedParameter or value_type in type_coercion_map: + return True + if dispatcher.resolve_type(value_type) is not None: + return True + return False def parameter_value_needs_processing( @@ -1887,7 +1965,31 @@ def parameter_value_needs_processing( return True if fallback_items is None: fallback_items = type_coercion_fallbacks(type_coercion_map) - return type_coercion_dispatcher(fallback_items).get(value) is not None + return type_coercion_dispatcher(fallback_items).resolve_type(value_type) is not None + + +def _resolve_fast_params( + parameters: "tuple[Any, ...]", kwargs: "dict[str, Any]" +) -> "tuple[Any, ...] | list[Any] | dict[str, Any] | None": + """Normalize execute(*parameters, **kwargs) arguments for fast-path cache lookup.""" + if kwargs: + if not parameters: + return kwargs + return None + param_len = len(parameters) + if param_len == 0: + return () + if param_len == 1: + first = parameters[0] + if isinstance(first, (tuple, list, dict)): + return first + if is_statement_filter(first): + return None + return parameters + for item in parameters: + if is_statement_filter(item) or isinstance(item, (tuple, list, dict)): + return None + return parameters def _raise_database_exception( @@ -2148,6 +2250,29 @@ def _check_declared_named_row(declared: "tuple[ParameterDeclaration, ...]", supp raise SQLSpecError(msg) +def _many_parameters_already_prepared( + parameters: "list[Any]", statement_config: "StatementConfig", applied_wrap_types: bool +) -> bool: + """Return True when execute_many parameters were already coerced during compile().""" + if statement_config.parameter_config.type_coercion_map: + return True + if applied_wrap_types: + return False + if not parameters: + return True + first = parameters[0] + if isinstance(first, (tuple, list)): + return not any(type(v) is TypedParameter for v in first) + if isinstance(first, dict): + return not any(type(v) is TypedParameter for v in first.values()) + return type(first) is not TypedParameter + + +def _row_needs_declared_default(declared: "tuple[ParameterDeclaration, ...]", supplied: "dict[str, Any]") -> bool: + """Return True when any optional declared parameter is missing from supplied.""" + return any(not declaration.required and declaration.name not in supplied for declaration in declared) + + def _check_declared_parameters(sql_statement: "SQL") -> None: """Enforce declared-parameter contracts on a copy of the user params. @@ -2162,18 +2287,28 @@ def _check_declared_parameters(sql_statement: "SQL") -> None: if sql_statement.is_many: rows = sql_statement.positional_parameters if rows and isinstance(rows[0], dict): - copied_rows: list[Any] = [] - for row in rows: + copied_rows: list[Any] | None = None + for idx, row in enumerate(rows): if isinstance(row, dict): - row_copy = dict(row) - _check_declared_named_row(declared, row_copy) - copied_rows.append(row_copy) - else: + if _row_needs_declared_default(declared, row): + row_copy = dict(row) + _check_declared_named_row(declared, row_copy) + if copied_rows is None: + copied_rows = list(rows[:idx]) + copied_rows.append(row_copy) + continue + _check_declared_named_row(declared, row) + if copied_rows is not None: copied_rows.append(row) - sql_statement._positional_parameters = copied_rows + if copied_rows is not None: + sql_statement._positional_parameters = copied_rows return if sql_statement.positional_parameters: return - named_copy = dict(sql_statement.named_parameters) - _check_declared_named_row(declared, named_copy) - sql_statement._named_parameters = named_copy + named_params = sql_statement.named_parameters + if _row_needs_declared_default(declared, named_params): + named_copy = dict(named_params) + _check_declared_named_row(declared, named_copy) + sql_statement._named_parameters = named_copy + else: + _check_declared_named_row(declared, named_params) diff --git a/sqlspec/driver/_query_cache.py b/sqlspec/driver/_query_cache.py index 0ce0d3cfc..f5233b1ad 100644 --- a/sqlspec/driver/_query_cache.py +++ b/sqlspec/driver/_query_cache.py @@ -1,18 +1,29 @@ """Query cache for fast-path statement execution.""" +import threading from collections import OrderedDict from typing import TYPE_CHECKING, Final, final from mypy_extensions import mypyc_attr +from sqlspec.core.parameters import ParameterProcessor, ParameterStyle + if TYPE_CHECKING: + from collections.abc import Mapping + from sqlspec.core.compiler import OperationProfile, OperationType from sqlspec.core.parameters import ParameterProfile - from sqlspec.core.statement import ProcessedState + from sqlspec.core.statement import SQL, ProcessedState, StatementConfig -__all__ = ("STMT_CACHE_MAX_SIZE", "CachedQuery", "QueryCache") +__all__ = ("STMT_CACHE_MAX_SIZE", "CachedQuery", "QueryCache", "StatementCacheHolder", "resolve_statement_cache_size") STMT_CACHE_MAX_SIZE: Final[int] = 1024 +_CACHED_NAMED_STYLES: Final[frozenset[str]] = frozenset(( + ParameterStyle.NAMED_COLON.value, + ParameterStyle.NAMED_AT.value, + ParameterStyle.NAMED_DOLLAR.value, + ParameterStyle.NAMED_PYFORMAT.value, +)) @final @@ -29,11 +40,13 @@ class CachedQuery: operation_profile: "OperationProfile" param_count: int processed_state: "ProcessedState" + needs_style_remap: bool __slots__ = ( "applied_wrap_types", "compiled_sql", "input_named_parameters", + "needs_style_remap", "operation_profile", "operation_type", "param_count", @@ -53,6 +66,7 @@ def __init__( operation_profile: "OperationProfile", param_count: int, processed_state: "ProcessedState", + needs_style_remap: bool | None = None, ) -> None: self.compiled_sql = compiled_sql self.parameter_profile = parameter_profile @@ -63,6 +77,11 @@ def __init__( self.operation_profile = operation_profile self.param_count = param_count self.processed_state = processed_state + self.needs_style_remap = ( + bool(_CACHED_NAMED_STYLES.intersection(parameter_profile.styles)) + if needs_style_remap is None + else needs_style_remap + ) @final @@ -70,31 +89,162 @@ def __init__( class QueryCache: """LRU cache for compiled query metadata.""" - __slots__ = ("_cache", "_max_size") + __slots__ = ("_cache", "_lock", "_max_size") def __init__(self, max_size: int = STMT_CACHE_MAX_SIZE) -> None: self._cache: OrderedDict[str, CachedQuery] = OrderedDict() + self._lock = threading.Lock() self._max_size = max_size def __len__(self) -> int: - return len(self._cache) + with self._lock: + return len(self._cache) def get(self, sql: str) -> "CachedQuery | None": - entry = self._cache.get(sql) - if entry is None: - return None - self._cache.move_to_end(sql) - return entry + with self._lock: + entry = self._cache.get(sql) + if entry is None: + return None + self._cache.move_to_end(sql) + return entry def set(self, sql: str, entry: "CachedQuery") -> None: if self._max_size <= 0: return - if sql in self._cache: - self._cache.move_to_end(sql) - elif len(self._cache) >= self._max_size: - self._cache.popitem(last=False) - self._cache[sql] = entry + with self._lock: + if sql in self._cache: + self._cache.move_to_end(sql) + elif len(self._cache) >= self._max_size: + self._cache.popitem(last=False) + self._cache[sql] = entry + + def clear(self) -> None: + """Clear all cached entries.""" + with self._lock: + self._cache.clear() + + +@final +@mypyc_attr(allow_interpreted_subclasses=False) +class StatementCacheHolder: + """Shared per-config holder for statement caches, rebind processor, and execution hints.""" + + statement_config: "StatementConfig" + max_size: int + query_cache: QueryCache + rebind_processor: ParameterProcessor + _statement_cache: "OrderedDict[str, SQL]" + _row_count_hints: "OrderedDict[str, int]" + _bulk_insert_sql: "OrderedDict[str, tuple[str, str]]" + _lock: threading.Lock + + __slots__ = ( + "_bulk_insert_sql", + "_lock", + "_row_count_hints", + "_statement_cache", + "max_size", + "query_cache", + "rebind_processor", + "statement_config", + ) + + def __init__(self, statement_config: "StatementConfig", max_size: int = STMT_CACHE_MAX_SIZE) -> None: + self.statement_config = statement_config + self.max_size = max_size + self.query_cache = QueryCache(max_size) + self.rebind_processor = ParameterProcessor( + converter=statement_config.parameter_converter, + validator=statement_config.parameter_validator, + cache_max_size=0, + validator_cache_max_size=0, + ) + self._statement_cache = OrderedDict() + self._row_count_hints = OrderedDict() + self._bulk_insert_sql = OrderedDict() + self._lock = threading.Lock() + + def get_statement(self, sql: str) -> "SQL | None": + """Retrieve a cached SQL object and mark it as recently used.""" + with self._lock: + entry = self._statement_cache.get(sql) + if entry is None: + return None + self._statement_cache.move_to_end(sql) + return entry + + def set_statement(self, sql: str, stmt: "SQL", max_size: int | None = None) -> None: + """Store a prepared SQL object in the bounded LRU cache.""" + effective_max = self.max_size if max_size is None else max_size + if effective_max <= 0: + return + with self._lock: + if sql in self._statement_cache: + self._statement_cache.move_to_end(sql) + else: + while len(self._statement_cache) >= effective_max: + self._statement_cache.popitem(last=False) + self._statement_cache[sql] = stmt + + def get_row_count_hint(self, compiled_sql: str) -> int | None: + """Return the last observed row count for a compiled SQL string.""" + with self._lock: + entry = self._row_count_hints.get(compiled_sql) + if entry is None: + return None + self._row_count_hints.move_to_end(compiled_sql) + return entry + + def set_row_count_hint(self, compiled_sql: str, row_count: int) -> None: + """Record the observed row count for a compiled SQL string.""" + if self.max_size <= 0: + return + with self._lock: + if compiled_sql in self._row_count_hints: + self._row_count_hints.move_to_end(compiled_sql) + elif len(self._row_count_hints) >= self.max_size: + self._row_count_hints.popitem(last=False) + self._row_count_hints[compiled_sql] = row_count + + def get_bulk_insert_sql(self, compiled_sql: str) -> "tuple[str, str] | None": + """Return cached (table_sql, columns_sql) fragments for a bulk INSERT statement.""" + with self._lock: + entry = self._bulk_insert_sql.get(compiled_sql) + if entry is None: + return None + self._bulk_insert_sql.move_to_end(compiled_sql) + return entry + + def set_bulk_insert_sql(self, compiled_sql: str, entry: "tuple[str, str]") -> None: + """Cache (table_sql, columns_sql) fragments for a bulk INSERT statement.""" + if self.max_size <= 0: + return + with self._lock: + if compiled_sql in self._bulk_insert_sql: + self._bulk_insert_sql.move_to_end(compiled_sql) + elif len(self._bulk_insert_sql) >= self.max_size: + self._bulk_insert_sql.popitem(last=False) + self._bulk_insert_sql[compiled_sql] = entry def clear(self) -> None: """Clear all cached entries.""" - self._cache.clear() + self.query_cache.clear() + with self._lock: + self._statement_cache.clear() + self._row_count_hints.clear() + self._bulk_insert_sql.clear() + + +def resolve_statement_cache_size(driver_features: "Mapping[str, object] | None") -> int: + """Resolve the statement cache capacity from driver_features.""" + if not driver_features: + return STMT_CACHE_MAX_SIZE + cache_size = driver_features.get("sqlspec_statement_cache_size", STMT_CACHE_MAX_SIZE) + if isinstance(cache_size, int): + return max(0, cache_size) + if isinstance(cache_size, str): + try: + return max(0, int(cache_size)) + except ValueError: + return STMT_CACHE_MAX_SIZE + return STMT_CACHE_MAX_SIZE diff --git a/sqlspec/driver/_sync.py b/sqlspec/driver/_sync.py index 52c26d6f6..e8b9411c5 100644 --- a/sqlspec/driver/_sync.py +++ b/sqlspec/driver/_sync.py @@ -1,6 +1,7 @@ """Synchronous driver protocol implementation.""" import logging +import sys from abc import abstractmethod from time import perf_counter from typing import TYPE_CHECKING, Any, ClassVar, Final, Generic, Literal, Self, TypeVar, cast, final, overload @@ -29,6 +30,7 @@ StackExecutionObserver, SyncExceptionHandler, _raise_database_exception, + _resolve_fast_params, describe_stack_statement, handle_single_row_error, validate_savepoint_name, @@ -54,7 +56,7 @@ from sqlglot.dialects.dialect import DialectType from sqlspec.builder import QueryBuilder - from sqlspec.core import ArrowResult, SQLResult, Statement, StatementConfig, StatementFilter + from sqlspec.core import ArrowResult, OperationType, SQLResult, Statement, StatementConfig, StatementFilter from sqlspec.data_dictionary import ( ColumnMetadata, ForeignKeyMetadata, @@ -211,81 +213,34 @@ def dispatch_statement_execution(self, statement: "SQL", connection: "Any") -> " if runtime is None or runtime.is_idle: exc_handler = self.handle_database_exceptions() - result = self._run_with_exception_handler( - exc_handler, - self._dispatch_statement_with_cursor, - connection, - statement, - has_execution_parameters=bool(execution_parameters), - ) + if sys.exc_info()[1] is None: + with exc_handler: + result = self._dispatch_statement_with_cursor( + connection, statement, has_execution_parameters=bool(execution_parameters) + ) + else: + result = self._run_with_exception_handler( + exc_handler, + self._dispatch_statement_with_cursor, + connection, + statement, + has_execution_parameters=bool(execution_parameters), + ) self._check_pending_exception(exc_handler) assert result is not None return result - operation = statement.operation_type - query_context = { - "sql": compiled_sql, - "parameters": execution_parameters, - "driver": type(self).__name__, - "operation": operation, - "is_many": statement.is_many, - "is_script": statement.is_script, - } - runtime.emit_query_start_sync(**query_context) - sql_hash = None - if runtime.span_manager.is_enabled or ( - runtime.has_statement_observers and runtime.config.logging and runtime.config.logging.include_sql_hash - ): - sql_hash = observability_runtime.compute_sql_hash(compiled_sql) - span = runtime.start_query_span(compiled_sql, operation, type(self).__name__, sql_hash=sql_hash) - started = perf_counter() - exc_handler = self.handle_database_exceptions() - try: - result = self._run_with_exception_handler( - exc_handler, - self._dispatch_statement_with_cursor, - connection, - statement, - has_execution_parameters=bool(execution_parameters), - ) - except Exception as exc: # pragma: no cover - pending_exception = exc_handler.pending_exception - if pending_exception is not None: - mapped_exc = pending_exception - runtime.span_manager.end_span(span, error=mapped_exc) - runtime.emit_error_sync(mapped_exc, **query_context) - _raise_database_exception(exc_handler, exc) - runtime.span_manager.end_span(span, error=exc) - runtime.emit_error_sync(exc, **query_context) - _raise_database_exception(exc_handler, exc) - - pending_exception = exc_handler.pending_exception - if pending_exception is not None: - mapped_exc = pending_exception - runtime.span_manager.end_span(span, error=mapped_exc) - runtime.emit_error_sync(mapped_exc, **query_context) - _raise_database_exception(exc_handler, None) - - assert result is not None - - runtime.span_manager.end_span(span) - duration = perf_counter() - started - runtime.emit_query_complete_sync(**{**query_context, "rows_affected": result.rows_affected}) - runtime.emit_statement_event( - sql=compiled_sql, - parameters=execution_parameters, - driver=type(self).__name__, - operation=operation, - execution_mode=self.statement_config.execution_mode, + return self._execute_with_observability_sync( + compiled_sql=compiled_sql, + execution_parameters=execution_parameters, + operation=statement.operation_type, is_many=statement.is_many, is_script=statement.is_script, - rows_affected=result.rows_affected, - duration_s=duration, - storage_backend=(result.metadata or {}).get("storage_backend"), - started_at=started, - precomputed_sql_hash=sql_hash, + runner=self._dispatch_statement_observed_sync, + connection=connection, + statement=statement, + has_execution_parameters=bool(execution_parameters), ) - return result finally: self._release_pooled_statement(statement) @@ -479,11 +434,7 @@ def execute( **kwargs: Any, ) -> "SQLResult": """Execute a statement with parameter handling.""" - fast_params: tuple[Any, ...] | list[Any] | dict[str, Any] | None = None - if not kwargs and len(parameters) == 1 and isinstance(parameters[0], (tuple, list, dict)): - fast_params = cast("tuple[Any, ...] | list[Any] | dict[str, Any]", parameters[0]) - elif not parameters and kwargs: - fast_params = kwargs + fast_params = _resolve_fast_params(parameters, kwargs) if ( self._stmt_cache_enabled and (statement_config is None or statement_config is self.statement_config) @@ -1508,6 +1459,90 @@ def _run_with_exception_handler( """Run a sync operation without inheriting the caller's active exception state.""" return _run_with_sync_exception_handler(exc_handler, operation, *args, **kwargs) + def _execute_with_observability_sync( + self, + *, + compiled_sql: str, + execution_parameters: Any, + operation: "OperationType", + is_many: bool, + is_script: bool, + runner: "Callable[..., SQLResult]", + **runner_kwargs: Any, + ) -> "SQLResult": + """Execute a sync runner while emitting spans, lifecycle hooks, and statement events.""" + runtime = self._observability + if runtime is None or runtime.is_idle: + return runner(**runner_kwargs) + driver_name = type(self).__name__ + query_context = { + "sql": compiled_sql, + "parameters": execution_parameters, + "driver": driver_name, + "operation": operation, + "is_many": is_many, + "is_script": is_script, + } + runtime.emit_query_start_sync(**query_context) + sql_hash = None + if runtime.span_manager.is_enabled or ( + runtime.has_statement_observers and runtime.config.logging and runtime.config.logging.include_sql_hash + ): + sql_hash = observability_runtime.compute_sql_hash(compiled_sql) + span = runtime.start_query_span(compiled_sql, operation, driver_name, sql_hash=sql_hash) + started = perf_counter() + try: + result = runner(**runner_kwargs) + except Exception as exc: + runtime.span_manager.end_span(span, error=exc) + runtime.emit_error_sync(exc, **query_context) + raise + runtime.span_manager.end_span(span) + duration = perf_counter() - started + runtime.emit_query_complete_sync(**{**query_context, "rows_affected": result.rows_affected}) + runtime.emit_statement_event( + sql=compiled_sql, + parameters=execution_parameters, + driver=driver_name, + operation=operation, + execution_mode=self.statement_config.execution_mode, + is_many=is_many, + is_script=is_script, + rows_affected=result.rows_affected, + duration_s=duration, + storage_backend=(result.metadata or {}).get("storage_backend"), + started_at=started, + precomputed_sql_hash=sql_hash, + ) + return result + + def _dispatch_statement_observed_sync( + self, connection: Any, statement: "SQL", *, has_execution_parameters: bool + ) -> "SQLResult": + """Execute a statement inside an exception handler and raise any mapped error.""" + exc_handler = self.handle_database_exceptions() + result: SQLResult | None = None + try: + if sys.exc_info()[1] is None: + with exc_handler: + result = self._dispatch_statement_with_cursor( + connection, statement, has_execution_parameters=has_execution_parameters + ) + else: + result = self._run_with_exception_handler( + exc_handler, + self._dispatch_statement_with_cursor, + connection, + statement, + has_execution_parameters=has_execution_parameters, + ) + except Exception as exc: # pragma: no cover + _raise_database_exception(exc_handler, exc) + if exc_handler.pending_exception is not None: + _raise_database_exception(exc_handler, None) + assert result is not None + return result + def _execute_cache_hit( self, sql: str, params: "tuple[Any, ...] | list[Any] | dict[str, Any]", cached: CachedQuery ) -> "SQLResult": @@ -1526,11 +1561,35 @@ def _execute_cache_hit( SQLResult or DMLResult. """ exc_handler = self.handle_database_exceptions() - result = self._run_with_exception_handler(exc_handler, self._execute_cache_hit_with_cursor, sql, params, cached) + result: SQLResult | None = None + if sys.exc_info()[1] is None: + with exc_handler: + result = self._execute_cache_hit_with_cursor(sql, params, cached) + else: + result = self._run_with_exception_handler( + exc_handler, self._execute_cache_hit_with_cursor, sql, params, cached + ) self._check_pending_exception(exc_handler) assert result is not None return result + def _execute_cache_hit_observed( + self, sql: str, params: "tuple[Any, ...] | list[Any] | dict[str, Any]", cached: CachedQuery + ) -> "SQLResult": + """Execute a fast-path cache hit wrapped in observability instrumentation.""" + obs_params = list(params) if isinstance(params, tuple) else params + return self._execute_with_observability_sync( + compiled_sql=cached.compiled_sql, + execution_parameters=obs_params, + operation=cached.operation_type, + is_many=False, + is_script=False, + runner=self._execute_cache_hit, + sql=sql, + params=params, + cached=cached, + ) + def _execute_cached_statement(self, statement: "SQL") -> "SQLResult": """Execute pre-compiled query via fast path. @@ -1538,16 +1597,36 @@ def _execute_cached_statement(self, statement: "SQL") -> "SQLResult": will hit the fast path in _compiled_statement (is_processed check). """ exc_handler = self.handle_database_exceptions() + result: SQLResult | None = None try: - result = self._run_with_exception_handler( - exc_handler, self._execute_cached_statement_with_cursor, statement - ) + if sys.exc_info()[1] is None: + with exc_handler: + result = self._execute_cached_statement_with_cursor(statement) + else: + result = self._run_with_exception_handler( + exc_handler, self._execute_cached_statement_with_cursor, statement + ) self._check_pending_exception(exc_handler) assert result is not None return result finally: self._release_pooled_statement(statement) + def _execute_cached_statement_observed( + self, statement: "SQL", compiled_sql: str, execution_parameters: Any, operation: "OperationType" + ) -> "SQLResult": + """Execute a rebound cached statement wrapped in observability instrumentation.""" + obs_params = list(execution_parameters) if isinstance(execution_parameters, tuple) else execution_parameters + return self._execute_with_observability_sync( + compiled_sql=compiled_sql, + execution_parameters=obs_params, + operation=operation, + is_many=False, + is_script=False, + runner=self._execute_cached_statement, + statement=statement, + ) + def _dispatch_statement_with_cursor( self, connection: Any, statement: "SQL", *, has_execution_parameters: bool ) -> "SQLResult": @@ -1597,31 +1676,31 @@ def _execute_cache_hit_with_cursor( result: SQLResult | None = None try: with self.with_cursor(self.connection) as cursor: - execute = getattr(cursor, "execute", None) - fetchall = getattr(cursor, "fetchall", None) returns_rows = cached.operation_profile.returns_rows + execute = getattr(cursor, "execute", None) if cursor is not self.connection else None + fetchall = getattr(cursor, "fetchall", None) if execute is not None and returns_rows else None can_use_cursor_fast_path = execute is not None and ( (returns_rows and fetchall is not None) or (not returns_rows and hasattr(cursor, "rowcount")) ) if can_use_cursor_fast_path: assert execute is not None - execute(cached.compiled_sql, params) + if params: + execute(cached.compiled_sql, params) + else: + execute(cached.compiled_sql) if returns_rows: assert fetchall is not None fetched_data = fetchall() data, column_names, row_count = self.collect_rows(cursor, fetched_data) - execution_result = self.create_execution_result( - cursor, + result = self.build_cached_select_result( + sql, + params, + cached, selected_data=data, column_names=column_names, data_row_count=row_count, - is_select_result=True, row_format=resolve_row_format(data), ) - direct_statement = self._cached_statement( - sql, params, cached, params, params_are_simple=True, compiled_sql=cached.compiled_sql - ) - result = self.build_statement_result(direct_statement, execution_result) else: affected_rows = self.resolve_rowcount(cursor) result = DMLResult(cached.operation_type, affected_rows) diff --git a/sqlspec/extensions/adk/memory/store.py b/sqlspec/extensions/adk/memory/store.py index ce0731618..8a437ac91 100644 --- a/sqlspec/extensions/adk/memory/store.py +++ b/sqlspec/extensions/adk/memory/store.py @@ -13,7 +13,7 @@ from sqlspec.utils.sync_tools import async_ if TYPE_CHECKING: - from collections.abc import Callable, Sequence + from collections.abc import Sequence from sqlspec.config import DatabaseConfigProtocol from sqlspec.extensions.adk.memory._types import StoredMemory @@ -31,9 +31,6 @@ class _ADKMemoryStoreCommon(Generic[ConfigT]): """Shared non-async ADK store state and helpers.""" - if TYPE_CHECKING: - _drop_memory_table_sql: "Callable[[], list[str]]" - __slots__ = ( "_config", "_enable_bm25", @@ -148,6 +145,10 @@ def _schema_management_flags(self) -> tuple[bool, bool]: create_schema = adk_config.get("create_schema", True) if isinstance(adk_config, dict) else True return bool(manage_schema), bool(create_schema) + def _drop_memory_table_sql(self) -> "list[str]": + """Get the DROP TABLE SQL statements for this database dialect.""" + raise NotImplementedError + def _drop_sql_for_table(self, table_name: str) -> list[str]: current_table = self._memory_table self._memory_table = table_name diff --git a/sqlspec/extensions/adk/store.py b/sqlspec/extensions/adk/store.py index e4caa6fcb..3b5af519a 100644 --- a/sqlspec/extensions/adk/store.py +++ b/sqlspec/extensions/adk/store.py @@ -14,8 +14,6 @@ from sqlspec.utils.sync_tools import async_ if TYPE_CHECKING: - from collections.abc import Callable - from sqlspec.config import DatabaseConfigProtocol from sqlspec.extensions.adk._types import SessionOrderBy, StoredEvent, StoredSession @@ -38,9 +36,6 @@ class _ADKStoreCommon(Generic[ConfigT]): """Shared non-async ADK store state and helpers.""" - if TYPE_CHECKING: - _drop_tables_sql: "Callable[[], list[str]]" - __slots__ = ( "_app_state_table", "_config", @@ -130,6 +125,10 @@ def create_schema_enabled(self) -> bool: manage_schema, create_schema = self._schema_management_flags() return manage_schema and create_schema + def _drop_tables_sql(self) -> "list[str]": + """Get the DROP TABLE SQL statements for this database dialect.""" + raise NotImplementedError + def _reset_drop_statements(self) -> "list[str]": """Return all table drops needed before recreating the clean-break schema.""" statements = list(self._drop_tables_sql()) diff --git a/sqlspec/utils/dispatch.py b/sqlspec/utils/dispatch.py index 862a8c630..8a117af69 100644 --- a/sqlspec/utils/dispatch.py +++ b/sqlspec/utils/dispatch.py @@ -2,7 +2,7 @@ from mypy_extensions import mypyc_attr -__all__ = ("TypeDispatcher",) +__all__ = ("_NO_MATCH", "TypeDispatcher") T = TypeVar("T") diff --git a/tests/integration/adapters/duckdb/duckdb/test_driver.py b/tests/integration/adapters/duckdb/duckdb/test_driver.py index e1b458397..da041ed14 100644 --- a/tests/integration/adapters/duckdb/duckdb/test_driver.py +++ b/tests/integration/adapters/duckdb/duckdb/test_driver.py @@ -9,6 +9,17 @@ from sqlspec.exceptions import SQLBuilderError from tests.conftest import requires_interpreted +__all__ = ( + "duckdb_session", + "test_duckdb_data_types", + "test_duckdb_error_handling_and_edge_cases", + "test_duckdb_integration_native_type_contract_parity", + "test_duckdb_schema_operations", + "test_duckdb_statement_stack_continue_on_error", + "test_duckdb_unsupported_lock_raises", + "test_duckdb_window_functions", +) + pytestmark = pytest.mark.xdist_group("duckdb") @@ -210,3 +221,31 @@ def test_duckdb_statement_stack_continue_on_error(duckdb_session: DuckDBDriver) verify = duckdb_session.execute("SELECT COUNT(*) AS total FROM test_table") assert verify.data is not None assert verify.get_data()[0]["total"] == 2 + + +@pytest.mark.parametrize( + "sql_expr", + [ + "1::HUGEINT", + "1::UHUGEINT", + "'101'::BIT", + "'12:00:00+00'::TIMETZ", + "'2026-01-01 00:00:00'::TIMESTAMP_NS", + "INTERVAL 1 DAY", + "'550e8400-e29b-41d4-a716-446655440000'::UUID", + "[1, 2, 3]::INT[3]", + "MAP {'a': 1}", + "[INTERVAL 1 DAY]", + "{'u': '550e8400-e29b-41d4-a716-446655440000'::UUID}", + "1::BIGNUM", + ], +) +def test_duckdb_integration_native_type_contract_parity(duckdb_session: DuckDBDriver, sql_expr: str) -> None: + """Integration check that first and cached DuckDB queries return raw fetchall() Python types.""" + expected = duckdb_session.connection.execute(f"SELECT {sql_expr} AS val").fetchall()[0][0] + query = f"SELECT {sql_expr} AS val WHERE ? >= 0" + for index in range(3): + result = duckdb_session.execute(query, (index,)) + actual = result.one()["val"] + assert type(actual) is type(expected) + assert actual == expected diff --git a/tests/unit/adapters/test_aiosqlite/test_pool.py b/tests/unit/adapters/test_aiosqlite/test_pool.py index cf6d81f70..403f1db80 100644 --- a/tests/unit/adapters/test_aiosqlite/test_pool.py +++ b/tests/unit/adapters/test_aiosqlite/test_pool.py @@ -5,9 +5,11 @@ from pathlib import Path from threading import Thread from typing import TYPE_CHECKING, Any, Literal, cast +from unittest.mock import AsyncMock import pytest +from sqlspec.adapters.aiosqlite import pool as pool_module from sqlspec.adapters.aiosqlite.pool import ( SQLITE_BUSY_TIMEOUT, SQLITE_DEFAULT_ENABLE_FOREIGN_KEYS, @@ -24,6 +26,28 @@ if TYPE_CHECKING: from sqlspec.adapters.aiosqlite._typing import AiosqliteConnection +__all__ = ( + "test_acquire_and_release_idle_fast_path", + "test_acquire_enforces_connect_timeout_on_pool_exhaustion", + "test_close_continues_after_a_native_connection_was_closed", + "test_concurrent_first_acquire_on_new_file_database_succeeds", + "test_default_pool_uses_native_like_shared_pragma_profile", + "test_disable_optimizations_preserves_native_memory_profile", + "test_file_pool_uses_wal_normal_and_shared_busy_timeout", + "test_file_pragma_script_fallback_replays_file_profile", + "test_optimization_and_foreign_key_flags_are_independent", + "test_post_connect_failure_closes_raw_connection", + "test_pragma_script_fallback_continues_after_optimization_failure", + "test_pragma_script_fallback_honors_independent_flags", + "test_pragma_script_fallback_surfaces_explicit_foreign_key_failure", + "test_pragma_script_fallback_uses_native_like_defaults", + "test_shared_memory_pragma_script_fallback_replays_read_uncommitted", + "test_shared_memory_preserves_read_uncommitted", + "test_sqlite_family_pragma_defaults_are_equal", + "test_wal_setup_failure_propagates_from_new_connection", + "test_wal_setup_retries_locks_and_propagates_errors", +) + pytest.importorskip("aiosqlite", reason="aiosqlite adapter requires the aiosqlite package") @@ -34,27 +58,6 @@ def test_sqlite_family_pragma_defaults_are_equal() -> None: assert SQLITE_DEFAULT_ENABLE_FOREIGN_KEYS is SYNC_SQLITE_DEFAULT_ENABLE_FOREIGN_KEYS is False -async def _read_pragma( - connection: "AiosqliteConnection", - statement: Literal[ - "PRAGMA foreign_keys", - "PRAGMA busy_timeout", - "PRAGMA cache_size", - "PRAGMA journal_mode", - "PRAGMA synchronous", - "PRAGMA temp_store", - "PRAGMA read_uncommitted", - ], -) -> int | str: - cursor = await connection.execute(statement) - try: - row = await cursor.fetchone() - assert row is not None - return cast("int | str", row[0]) - finally: - await cursor.close() - - async def test_default_pool_uses_native_like_shared_pragma_profile() -> None: pool = AiosqliteConnectionPool({"database": ":memory:", "timeout": 30.0}) pool_connection = await pool.acquire() @@ -155,44 +158,7 @@ async def test_shared_memory_preserves_read_uncommitted() -> None: await pool.close() -class _FallbackConnection: - def __init__(self, fail_statement: str | None = None, *, script_fails: bool = True) -> None: - self.executed: list[str] = [] - self.fail_statement = fail_statement - self.script_fails = script_fails - self.closed = False - - async def executescript(self, _script: str) -> None: - if self.script_fails: - raise RuntimeError("script unavailable") - - async def execute(self, statement: str) -> None: - self.executed.append(statement) - if statement == self.fail_statement: - raise RuntimeError("individual pragma failed") - - async def commit(self) -> None: - return None - - async def close(self) -> None: - self.closed = True - - -class _FallbackConnectProxy: - def __init__(self, connection: _FallbackConnection) -> None: - self._thread = Thread(target=lambda: None) - self._connection = connection - - def __await__(self) -> Any: - async def resolve() -> _FallbackConnection: - return self._connection - - return resolve().__await__() - - async def test_pragma_script_fallback_uses_native_like_defaults(monkeypatch: pytest.MonkeyPatch) -> None: - from sqlspec.adapters.aiosqlite import pool as pool_module - connection = _FallbackConnection() monkeypatch.setattr(pool_module.aiosqlite, "connect", lambda **_: _FallbackConnectProxy(connection)) pool = AiosqliteConnectionPool({"database": ":memory:"}) @@ -210,8 +176,6 @@ async def test_pragma_script_fallback_uses_native_like_defaults(monkeypatch: pyt async def test_pragma_script_fallback_honors_independent_flags(monkeypatch: pytest.MonkeyPatch) -> None: - from sqlspec.adapters.aiosqlite import pool as pool_module - connection = _FallbackConnection() monkeypatch.setattr(pool_module.aiosqlite, "connect", lambda **_: _FallbackConnectProxy(connection)) pool = AiosqliteConnectionPool({"database": ":memory:"}, enable_optimizations=False, enable_foreign_keys=True) @@ -223,8 +187,6 @@ async def test_pragma_script_fallback_honors_independent_flags(monkeypatch: pyte async def test_pragma_script_fallback_surfaces_explicit_foreign_key_failure(monkeypatch: pytest.MonkeyPatch) -> None: - from sqlspec.adapters.aiosqlite import pool as pool_module - connection = _FallbackConnection(fail_statement="PRAGMA foreign_keys = ON") monkeypatch.setattr(pool_module.aiosqlite, "connect", lambda **_: _FallbackConnectProxy(connection)) pool = AiosqliteConnectionPool({"database": ":memory:"}, enable_foreign_keys=True) @@ -244,8 +206,6 @@ async def test_pragma_script_fallback_surfaces_explicit_foreign_key_failure(monk async def test_pragma_script_fallback_continues_after_optimization_failure(monkeypatch: pytest.MonkeyPatch) -> None: - from sqlspec.adapters.aiosqlite import pool as pool_module - connection = _FallbackConnection(fail_statement="PRAGMA synchronous = OFF") monkeypatch.setattr(pool_module.aiosqlite, "connect", lambda **_: _FallbackConnectProxy(connection)) pool = AiosqliteConnectionPool({"database": ":memory:"}) @@ -263,8 +223,6 @@ async def test_pragma_script_fallback_continues_after_optimization_failure(monke async def test_file_pragma_script_fallback_replays_file_profile(monkeypatch: pytest.MonkeyPatch) -> None: - from sqlspec.adapters.aiosqlite import pool as pool_module - connection = _FallbackConnection() monkeypatch.setattr(pool_module.aiosqlite, "connect", lambda **_: _FallbackConnectProxy(connection)) pool = AiosqliteConnectionPool({"database": "profile.db", "timeout": 30.0}) @@ -280,8 +238,6 @@ async def test_file_pragma_script_fallback_replays_file_profile(monkeypatch: pyt async def test_shared_memory_pragma_script_fallback_replays_read_uncommitted(monkeypatch: pytest.MonkeyPatch) -> None: - from sqlspec.adapters.aiosqlite import pool as pool_module - connection = _FallbackConnection() monkeypatch.setattr(pool_module.aiosqlite, "connect", lambda **_: _FallbackConnectProxy(connection)) pool = AiosqliteConnectionPool({"database": "file:fallback?mode=memory&cache=shared", "uri": True, "timeout": 30.0}) @@ -303,8 +259,6 @@ async def test_shared_memory_pragma_script_fallback_replays_read_uncommitted(mon async def test_post_connect_failure_closes_raw_connection( error: BaseException, monkeypatch: pytest.MonkeyPatch ) -> None: - from sqlspec.adapters.aiosqlite import pool as pool_module - connection = _FallbackConnection(script_fails=False) monkeypatch.setattr(pool_module.aiosqlite, "connect", lambda **_: _FallbackConnectProxy(connection)) @@ -362,10 +316,6 @@ async def acquire(pool: AiosqliteConnectionPool) -> str | int: async def test_wal_setup_retries_locks_and_propagates_errors( monkeypatch: pytest.MonkeyPatch, message: str, failures: int, calls: int ) -> None: - from unittest.mock import AsyncMock - - from sqlspec.adapters.aiosqlite import pool as pool_module - connection = AsyncMock() connection.execute.side_effect = [sqlite3.OperationalError(message)] * failures + [None] monkeypatch.setattr(pool_module.asyncio, "sleep", AsyncMock()) @@ -380,10 +330,6 @@ async def test_wal_setup_retries_locks_and_propagates_errors( async def test_wal_setup_failure_propagates_from_new_connection( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - from unittest.mock import AsyncMock - - from sqlspec.adapters.aiosqlite import pool as pool_module - monkeypatch.setattr( pool_module, "_enable_wal", AsyncMock(side_effect=sqlite3.OperationalError("database is locked")) ) @@ -407,3 +353,112 @@ async def test_close_continues_after_a_native_connection_was_closed() -> None: finally: await second.connection.close() await pool.close() + + +async def test_acquire_and_release_idle_fast_path(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """Warm idle connection acquire skips asyncio.wait_for and clean release skips end_transaction.""" + pool = AiosqliteConnectionPool({"database": str(tmp_path / "fast_path.sqlite")}, pool_size=2, min_size=1) + try: + async with pool.get_connection() as conn: + cursor = await conn.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)") + await cursor.close() + await conn.commit() + + wait_for_calls = 0 + end_tx_calls = 0 + original_wait_for = pool_module.asyncio.wait_for + original_end_tx = pool_module.end_transaction + + async def spy_wait_for(aw: Any, timeout: float | None = None) -> Any: + nonlocal wait_for_calls + wait_for_calls += 1 + return await original_wait_for(aw, timeout=timeout) + + async def spy_end_tx(connection: Any, *, commit: bool, supports_autocommit: bool | None = None) -> None: + nonlocal end_tx_calls + end_tx_calls += 1 + await original_end_tx(connection, commit=commit, supports_autocommit=supports_autocommit) + + monkeypatch.setattr(pool_module.asyncio, "wait_for", spy_wait_for) + monkeypatch.setattr(pool_module, "end_transaction", spy_end_tx) + + for _ in range(10): + async with pool.get_connection() as conn: + cursor = await conn.execute("SELECT 1") + row = await cursor.fetchone() + await cursor.close() + assert row == (1,) + + assert wait_for_calls == 0 + assert end_tx_calls == 0 + + async with pool.get_connection() as conn: + cursor = await conn.execute("INSERT INTO items (name) VALUES ('dirty')") + await cursor.close() + assert conn.in_transaction is True + + assert end_tx_calls == 1 + + async with pool.get_connection() as conn: + cursor = await conn.execute("SELECT COUNT(*) FROM items") + row = await cursor.fetchone() + await cursor.close() + assert row == (0,) + finally: + await pool.close() + + +async def _read_pragma( + connection: "AiosqliteConnection", + statement: Literal[ + "PRAGMA foreign_keys", + "PRAGMA busy_timeout", + "PRAGMA cache_size", + "PRAGMA journal_mode", + "PRAGMA synchronous", + "PRAGMA temp_store", + "PRAGMA read_uncommitted", + ], +) -> int | str: + cursor = await connection.execute(statement) + try: + row = await cursor.fetchone() + assert row is not None + return cast("int | str", row[0]) + finally: + await cursor.close() + + +class _FallbackConnection: + def __init__(self, fail_statement: str | None = None, *, script_fails: bool = True) -> None: + self.executed: list[str] = [] + self.fail_statement = fail_statement + self.script_fails = script_fails + self.closed = False + + async def executescript(self, _script: str) -> None: + if self.script_fails: + raise RuntimeError("script unavailable") + + async def execute(self, statement: str) -> None: + self.executed.append(statement) + if statement == self.fail_statement: + raise RuntimeError("individual pragma failed") + + async def commit(self) -> None: + return None + + async def close(self) -> None: + self.closed = True + + +class _FallbackConnectProxy: + def __init__(self, connection: _FallbackConnection) -> None: + self._thread = Thread(target=lambda: None) + self._connection = connection + + def __await__(self) -> Any: + async def resolve() -> _FallbackConnection: + return self._connection + + return resolve().__await__() diff --git a/tests/unit/adapters/test_duckdb/test_driver.py b/tests/unit/adapters/test_duckdb/test_driver.py new file mode 100644 index 000000000..cb73ffc7e --- /dev/null +++ b/tests/unit/adapters/test_duckdb/test_driver.py @@ -0,0 +1,203 @@ +"""Unit tests for DuckDBDriver execution and materialization behavior.""" + +from pathlib import Path +from typing import Any, Self + +import duckdb +import pytest +from sqlglot import exp + +from sqlspec.adapters.duckdb import DuckDBConfig +from sqlspec.core.result import SQLResult + +__all__ = ( + "test_duckdb_execute_many_caches_bulk_insert_sql_fragments", + "test_duckdb_materialization_arrow_vs_fetchall_threshold", + "test_duckdb_native_type_contract_parity", + "test_duckdb_unparseable_select_returns_rows_across_repeated_calls", +) + +DIVERGENT_AND_STANDARD_TYPE_EXPRESSIONS: tuple[str, ...] = ( + "1::HUGEINT", + "1::UHUGEINT", + "'101'::BIT", + "'12:00:00+00'::TIMETZ", + "'2026-01-01 00:00:00'::TIMESTAMP_NS", + "INTERVAL 1 DAY", + "'550e8400-e29b-41d4-a716-446655440000'::UUID", + "[1, 2, 3]::INT[3]", + "MAP {'a': 1}", + "[INTERVAL 1 DAY]", + "{'u': '550e8400-e29b-41d4-a716-446655440000'::UUID}", + "1::BIGNUM", + "1::INTEGER", + "1::BIGINT", + "'hello'::VARCHAR", + "1.5::DOUBLE", + "TRUE::BOOLEAN", + "DATE '2026-01-01'", + "TIMESTAMP '2026-01-01 00:00:00'", + "[1, 2, 3]", + "{'a': 1}", +) + + +def test_duckdb_unparseable_select_returns_rows_across_repeated_calls(tmp_path: Path) -> None: + """Repeated SELECT queries with qmark casts that fail sqlglot parsing must return rows on every call.""" + config = DuckDBConfig(connection_config={"database": str(tmp_path / "unparseable_select.duckdb")}) + try: + with config.provide_session() as session: + for value in (1, 2, 3): + result = session.execute("SELECT ?::INTEGER AS i", (value,)) + assert isinstance(result, SQLResult) + assert result.operation_type == "SELECT" + assert result.get_data() == [{"i": value}] + finally: + config.close_pool() + + +@pytest.mark.parametrize("sql_expr", DIVERGENT_AND_STANDARD_TYPE_EXPRESSIONS) +def test_duckdb_native_type_contract_parity(tmp_path: Path, sql_expr: str) -> None: + """First and cached executions must return identical Python types matching raw duckdb fetchall().""" + raw_conn = duckdb.connect() + try: + expected = raw_conn.execute(f"SELECT {sql_expr} AS val").fetchall()[0][0] + finally: + raw_conn.close() + + config = DuckDBConfig(connection_config={"database": str(tmp_path / "type_contract.duckdb")}) + try: + with config.provide_session() as session: + parameterized_sql = f"SELECT {sql_expr} AS val WHERE ? >= 0" + for index in range(3): + result = session.execute(parameterized_sql, (index,)) + actual = result.one()["val"] + assert type(actual) is type(expected) + assert actual == expected + + unparameterized_sql = f"SELECT {sql_expr} AS val" + for _ in range(2): + result = session.execute(unparameterized_sql) + actual = result.one()["val"] + assert type(actual) is type(expected) + assert actual == expected + finally: + config.close_pool() + + +def test_duckdb_materialization_arrow_vs_fetchall_threshold(tmp_path: Path) -> None: + """Queries with >= 64 non-divergent rows use Arrow; < 64 rows after observation and divergent columns use fetchall.""" + config = DuckDBConfig(connection_config={"database": str(tmp_path / "threshold.duckdb")}) + try: + with config.provide_session() as session: + session.execute("CREATE TABLE items AS SELECT i AS id, 'item_' || i::VARCHAR AS name FROM range(200) t(i)") + raw_connection = session.connection + counts = {"arrow": 0, "fetchall": 0} + + def reset_counts() -> None: + counts["arrow"] = 0 + counts["fetchall"] = 0 + + session.connection = _SpyDuckDBCursor(raw_connection, counts) # type: ignore[assignment] + try: + large_sql = "SELECT id, name FROM items WHERE id >= ?" + reset_counts() + res1 = session.execute(large_sql, (0,)) + assert len(res1.get_data()) == 200 + assert counts == {"arrow": 1, "fetchall": 0} + + reset_counts() + res2 = session.execute(large_sql, (0,)) + assert len(res2.get_data()) == 200 + assert counts == {"arrow": 1, "fetchall": 0} + + small_sql = "SELECT id, name FROM items WHERE id = ?" + reset_counts() + small1 = session.execute(small_sql, (1,)) + assert small1.one() == {"id": 1, "name": "item_1"} + assert counts == {"arrow": 1, "fetchall": 0} + + reset_counts() + small2 = session.execute(small_sql, (2,)) + assert small2.one() == {"id": 2, "name": "item_2"} + assert counts == {"arrow": 0, "fetchall": 1} + + divergent_sql = "SELECT id::HUGEINT AS big_id FROM items WHERE id >= ?" + reset_counts() + div1 = session.execute(divergent_sql, (0,)) + assert len(div1.get_data()) == 200 + assert type(div1.get_data()[0]["big_id"]) is int + assert counts == {"arrow": 0, "fetchall": 1} + + reset_counts() + div2 = session.execute(divergent_sql, (0,)) + assert len(div2.get_data()) == 200 + assert type(div2.get_data()[0]["big_id"]) is int + assert counts == {"arrow": 0, "fetchall": 1} + finally: + session.connection = raw_connection + finally: + config.close_pool() + + +def test_duckdb_execute_many_caches_bulk_insert_sql_fragments(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Repeated execute_many bulk inserts cache (target_table, column_sql) and skip sqlglot .sql() on subsequent batches.""" + config = DuckDBConfig(connection_config={"database": str(tmp_path / "bulk_cache.duckdb")}) + try: + with config.provide_session() as session: + session.execute("CREATE TABLE bulk_items (id INTEGER, name VARCHAR)") + insert_sql = "INSERT INTO bulk_items (id, name) VALUES (?, ?)" + res1 = session.execute_many(insert_sql, [(1, "a"), (2, "b")]) + assert res1.rows_affected == 2 + cached_pair = session._cache_holder.get_bulk_insert_sql(insert_sql) + assert cached_pair == ("bulk_items", "id, name") + + def fail_sql(*_args: Any, **_kwargs: Any) -> str: + msg = "exp.Expr.sql should not be called on bulk insert cache hit" + raise AssertionError(msg) + + monkeypatch.setattr(exp.Expr, "sql", fail_sql) + res2 = session.execute_many(insert_sql, [(3, "c"), (4, "d")]) + assert res2.rows_affected == 2 + count = session.select_value("SELECT COUNT(*) FROM bulk_items") + assert count == 4 + finally: + config.close_pool() + + +class _SpyDuckDBCursor: + """Proxy cursor for counting to_arrow_table and fetchall calls.""" + + __slots__ = ("_connection", "_counts") + + def __init__(self, connection: Any, counts: dict[str, int]) -> None: + self._connection = connection + self._counts = counts + + def __enter__(self) -> Self: + return self + + def __exit__(self, *_: Any) -> None: + return None + + @property + def description(self) -> Any: + return self._connection.description + + def execute(self, sql: str, parameters: Any = None) -> Self: + if parameters is not None: + self._connection.execute(sql, parameters) + else: + self._connection.execute(sql) + return self + + def fetchall(self) -> list[Any]: + self._counts["fetchall"] += 1 + return list(self._connection.fetchall()) + + def to_arrow_table(self) -> Any: + self._counts["arrow"] += 1 + return self._connection.to_arrow_table() + + def fetchone(self) -> Any: + return self._connection.fetchone() diff --git a/tests/unit/adapters/test_sync_adapters.py b/tests/unit/adapters/test_sync_adapters.py index 27929f126..af5834764 100644 --- a/tests/unit/adapters/test_sync_adapters.py +++ b/tests/unit/adapters/test_sync_adapters.py @@ -87,7 +87,7 @@ def test_sync_driver_fast_path_flag_disabled_by_observability() -> None: driver.attach_observability(runtime) - assert driver._stmt_cache_enabled is False + assert driver._stmt_cache_enabled is True finally: conn.close() diff --git a/tests/unit/core/test_compiler.py b/tests/unit/core/test_compiler.py index 785e7b115..cdfd2e8c5 100644 --- a/tests/unit/core/test_compiler.py +++ b/tests/unit/core/test_compiler.py @@ -1265,3 +1265,35 @@ def test_compiler_cache_isolates_expression_mutations(basic_statement_config: "S res2 = processor.compile(sql, None) assert res2.expression is not None assert res2.expression.args.get("where") is None + + +@requires_interpreted +def test_shared_expression_skips_ast_copy_on_compiler_and_parse_cache_hits( + basic_statement_config: "StatementConfig", +) -> None: + """Warm SQLProcessor compiled-cache and parse-cache hits must not call exp.Expr.copy().""" + sql = "WITH cte AS (SELECT id, name FROM users WHERE active = ?) SELECT id, name FROM cte WHERE id = ?" + processor = SQLProcessor(basic_statement_config) + processor.compile(sql, [1, 2]) + + parse_only_processor = SQLProcessor(basic_statement_config, cache_enabled=False, parse_cache_size=16) + parse_only_processor.compile(sql, [1, 2]) + + copy_calls = 0 + original_copy = exp.Expression.copy + + def counting_copy(self: "exp.Expression") -> "exp.Expression": + nonlocal copy_calls + copy_calls += 1 + return original_copy(self) + + with patch.object(exp.Expression, "copy", counting_copy): + for i in range(5): + res = processor.compile(sql, [1, i]) + assert res.expression is not None + assert copy_calls == 0 + + for i in range(5): + res = parse_only_processor.compile(sql, [1, i]) + assert res.expression is not None + assert copy_calls == 0 diff --git a/tests/unit/core/test_statement.py b/tests/unit/core/test_statement.py index 050b10408..174e17eea 100644 --- a/tests/unit/core/test_statement.py +++ b/tests/unit/core/test_statement.py @@ -1785,3 +1785,72 @@ def test_statement_config_equality_checks_all_parameter_config_fields() -> None: ) ) assert cfg1 != cfg2 + + +@requires_interpreted +def test_shared_expression_skips_ast_copy_on_cache_hit_and_rebind() -> None: + """Warm compiler cache hits and parameter rebinds must not deep-copy the sqlglot AST.""" + reset_statement_pipeline_cache() + config = StatementConfig(dialect="sqlite", parameter_config=DEFAULT_PARAMETER_CONFIG) + cte_sql = ( + "WITH filtered AS (SELECT id, name FROM users WHERE id >= ?) " + "SELECT f.id, f.name FROM filtered f WHERE f.id <= ?" + ) + warm_stmt = SQL(cte_sql, 1, 10, statement_config=config) + warm_stmt.compile() + + copy_calls = 0 + original_copy = exp.Expression.copy + + def counting_copy(self: "exp.Expression") -> "exp.Expression": + nonlocal copy_calls + copy_calls += 1 + return original_copy(self) + + with patch.object(exp.Expression, "copy", counting_copy): + for i in range(5): + hit_stmt = SQL(cte_sql, i, i + 10, statement_config=config) + hit_stmt.compile() + assert hit_stmt._peek_expression() is not None + assert copy_calls == 0 + + for i in range(5): + rebound = warm_stmt.copy(parameters=(i, i + 20)) + rebound.compile() + assert rebound._peek_expression() is not None + assert copy_calls == 0 + + +def test_copy_on_access_expression_isolates_cached_and_rebound_statements() -> None: + """Public expression accessors must copy once on first access and isolate mutations.""" + reset_statement_pipeline_cache() + config = StatementConfig(dialect="sqlite", parameter_config=DEFAULT_PARAMETER_CONFIG) + sql = "SELECT id, name FROM users WHERE id = ?" + + SQL(sql, 0, statement_config=config).compile() + stmt1 = SQL(sql, 1, statement_config=config) + stmt1.compile() + stmt2 = SQL(sql, 2, statement_config=config) + stmt2.compile() + rebound = stmt1.copy(parameters=(3,)) + rebound.compile() + + assert stmt1._peek_expression() is stmt2._peek_expression() + assert stmt1._peek_expression() is rebound._peek_expression() + + owned1 = stmt1.expression + assert owned1 is not None + assert stmt1.expression is owned1 + assert stmt1.statement_expression is owned1 + assert owned1 is not stmt2._peek_expression() + + owned1.set("limit", exp.Limit(expression=exp.Literal.number(1))) + + owned2 = stmt2.expression + assert owned2 is not None + assert owned2.args.get("limit") is None + + rebound_expr = rebound.statement_expression + assert rebound_expr is not None + assert rebound_expr is rebound.expression + assert rebound_expr.args.get("limit") is None diff --git a/tests/unit/driver/test_query_cache.py b/tests/unit/driver/test_query_cache.py index 6d8a9d654..9341b3c3b 100644 --- a/tests/unit/driver/test_query_cache.py +++ b/tests/unit/driver/test_query_cache.py @@ -1,15 +1,25 @@ # pyright: reportPrivateUsage = false """Tests for SQL query caching functionality.""" +import sqlite3 +import threading from collections import UserDict from collections.abc import AsyncIterator, Generator from contextlib import asynccontextmanager, contextmanager -from typing import Any +from datetime import date +from decimal import Decimal +from typing import Any, cast from unittest.mock import AsyncMock, Mock +from uuid import UUID, uuid4 import pytest from sqlglot import parse_one +from sqlspec import ObservabilityConfig +from sqlspec.adapters.aiosqlite import AiosqliteConfig +from sqlspec.adapters.sqlite import SqliteConfig, SqliteDriver +from sqlspec.base import SQLSpec +from sqlspec.config import LifecycleConfig from sqlspec.core import ( SQL, CachedStatement, @@ -18,14 +28,22 @@ ParameterInfo, ParameterProfile, ParameterStyle, + ParameterStyleConfig, ProcessedState, + StatementConfig, + TypedParameter, clear_all_caches, get_cache, ) -from sqlspec.core.parameters._processor import ParameterProcessor +from sqlspec.core.filters import LimitOffsetFilter +from sqlspec.core.parameters._declared import ParameterDeclaration +from sqlspec.core.parameters._processor import ParameterProcessor, type_coercion_fallbacks +from sqlspec.driver._async import AsyncDriverAdapterBase +from sqlspec.driver._common import _check_declared_parameters from sqlspec.driver._query_cache import CachedQuery, QueryCache from sqlspec.driver._sync import SyncDriverAdapterBase from sqlspec.exceptions import SQLSpecError +from sqlspec.observability import LoggingConfig, StatementEvent def _make_cached( @@ -489,7 +507,7 @@ def cached_sync_cursor(sqlite_sync_driver: Any, monkeypatch: Any) -> Generator[A cursor = Mock(spec=["execute", "fetchall", "rowcount", "description"]) cursor.rowcount = 0 - def execute(sql: str, params: Any) -> None: + def execute(sql: str, params: Any = ()) -> None: raw_cursor.execute(sql, params) cursor.description = raw_cursor.description cursor.rowcount = raw_cursor.rowcount @@ -531,6 +549,9 @@ def execute_then_error(sql: str, params: Any) -> None: cursor.execute = execute_then_error if stage == "fetchall": cursor.fetchall = Mock(side_effect=error) + elif stage == "build_statement_result": + monkeypatch.setattr(driver, "build_statement_result", Mock(side_effect=error)) + monkeypatch.setattr(driver, "build_cached_select_result", Mock(side_effect=error)) elif stage != "execute": monkeypatch.setattr(driver, stage, Mock(side_effect=error)) returns_rows = stage not in {"execute", "resolve_rowcount"} @@ -621,7 +642,7 @@ def cached_async_cursor( cursor = Mock(spec=["execute", "fetchall", "rowcount", "description"]) cursor.rowcount = 0 - def execute(sql: str, params: Any) -> None: + def execute(sql: str, params: Any = ()) -> None: raw_cursor.execute(sql, params) cursor.description = raw_cursor.description cursor.rowcount = raw_cursor.rowcount @@ -711,6 +732,9 @@ async def execute_async(sql: str, params: Any) -> None: cursor.execute = execute_async if stage == "fetchall": cursor.fetchall = Mock(side_effect=error) + elif stage == "build_statement_result": + monkeypatch.setattr(driver, "build_statement_result", Mock(side_effect=error)) + monkeypatch.setattr(driver, "build_cached_select_result", Mock(side_effect=error)) elif stage != "execute": monkeypatch.setattr(driver, stage, Mock(side_effect=error)) @@ -960,3 +984,655 @@ def test_refresh_statement_cache_state_allows_none_observability(sqlite_sync_dri finally: sqlite_sync_driver._observability = original_obs sqlite_sync_driver._refresh_statement_cache_state() + + +def test_cache_statement_command_not_cached(sqlite_sync_driver: Any) -> None: + """_cache_statement must refuse statements with undetermined COMMAND operation or missing AST.""" + sqlite_sync_driver._stmt_cache_enabled = True + + unparsed = SQL("SELECT ?::INTEGER AS i", 1, statement_config=sqlite_sync_driver.statement_config) + unparsed._processed_state = ProcessedState( + compiled_sql="SELECT ?::INTEGER AS i", execution_parameters=[1], parsed_expression=None, operation_type="SELECT" + ) + sqlite_sync_driver._cache_statement(unparsed) + assert sqlite_sync_driver._stmt_cache.get("SELECT ?::INTEGER AS i") is None + + command_stmt = SQL("VACUUM", statement_config=sqlite_sync_driver.statement_config) + command_stmt.compile() + sqlite_sync_driver._cache_statement(command_stmt) + assert sqlite_sync_driver._stmt_cache.get("VACUUM") is None + + +def test_fast_path_shapes_sync(sqlite_sync_driver: Any, monkeypatch: Any) -> None: + """Parameterless and scalar/varargs positional calls route through _cached_execution while StatementFilter bypasses it.""" + sqlite_sync_driver._stmt_cache_enabled = True + sqlite_sync_driver.execute("CREATE TABLE t_shapes (id INTEGER, label TEXT)") + sqlite_sync_driver.execute("INSERT INTO t_shapes (id, label) VALUES (?, ?)", (1, "one")) + + cache_hits: list[str] = [] + original_hit = sqlite_sync_driver._execute_cache_hit + + def tracked_hit(sql: str, params: Any, cached: CachedQuery) -> Any: + cache_hits.append(sql) + return original_hit(sql, params, cached) + + monkeypatch.setattr(sqlite_sync_driver, "_execute_cache_hit", tracked_hit) + + r1_no_param = sqlite_sync_driver.execute("SELECT 1 AS v") + r2_no_param = sqlite_sync_driver.execute("SELECT 1 AS v") + assert r1_no_param.get_data() == r2_no_param.get_data() == [{"v": 1}] + assert "SELECT 1 AS v" in cache_hits + + cache_hits.clear() + assert sqlite_sync_driver.select_value("SELECT ? AS v", 42) == 42 + assert sqlite_sync_driver.select_value("SELECT ? AS v", 99) == 99 + assert "SELECT ? AS v" in cache_hits + + cache_hits.clear() + assert sqlite_sync_driver.select_value("SELECT ? AS s", "hello") == "hello" + assert sqlite_sync_driver.select_value("SELECT ? AS s", "world") == "world" + assert "SELECT ? AS s" in cache_hits + + cache_hits.clear() + assert sqlite_sync_driver.select_value("SELECT ? AS b", b"bytes") == b"bytes" + assert sqlite_sync_driver.select_value("SELECT ? AS b", b"more") == b"more" + assert "SELECT ? AS b" in cache_hits + + cache_hits.clear() + assert sqlite_sync_driver.select_value("SELECT ? AS n", None) is None + assert sqlite_sync_driver.select_value("SELECT ? AS n", None) is None + assert "SELECT ? AS n" in cache_hits + + cache_hits.clear() + assert sqlite_sync_driver.select_one("SELECT ? AS a, ? AS b", 1, "two") == {"a": 1, "b": "two"} + assert sqlite_sync_driver.select_one("SELECT ? AS a, ? AS b", 3, "four") == {"a": 3, "b": "four"} + assert "SELECT ? AS a, ? AS b" in cache_hits + + cache_hits.clear() + r1_filtered = sqlite_sync_driver.execute("SELECT id, label FROM t_shapes", LimitOffsetFilter(10, 0)) + r2_filtered = sqlite_sync_driver.execute("SELECT id, label FROM t_shapes", LimitOffsetFilter(10, 0)) + assert r1_filtered.get_data() == r2_filtered.get_data() == [{"id": 1, "label": "one"}] + assert cache_hits == [] + + +@pytest.mark.anyio +async def test_fast_path_shapes_async(aiosqlite_async_driver: Any, monkeypatch: Any) -> None: + """Async parameterless and scalar/varargs positional calls route through _cached_execution while StatementFilter bypasses it.""" + aiosqlite_async_driver._stmt_cache_enabled = True + await aiosqlite_async_driver.execute("CREATE TABLE t_shapes (id INTEGER, label TEXT)") + await aiosqlite_async_driver.execute("INSERT INTO t_shapes (id, label) VALUES (?, ?)", (1, "one")) + + cache_hits: list[str] = [] + original_hit = aiosqlite_async_driver._execute_cache_hit + + async def tracked_hit(sql: str, params: Any, cached: CachedQuery) -> Any: + cache_hits.append(sql) + return await original_hit(sql, params, cached) + + monkeypatch.setattr(aiosqlite_async_driver, "_execute_cache_hit", tracked_hit) + + r1_no_param = await aiosqlite_async_driver.execute("SELECT 1 AS v") + r2_no_param = await aiosqlite_async_driver.execute("SELECT 1 AS v") + assert r1_no_param.get_data() == r2_no_param.get_data() == [{"v": 1}] + assert "SELECT 1 AS v" in cache_hits + + cache_hits.clear() + assert await aiosqlite_async_driver.select_value("SELECT ? AS v", 42) == 42 + assert await aiosqlite_async_driver.select_value("SELECT ? AS v", 99) == 99 + assert "SELECT ? AS v" in cache_hits + + cache_hits.clear() + assert await aiosqlite_async_driver.select_value("SELECT ? AS s", "hello") == "hello" + assert await aiosqlite_async_driver.select_value("SELECT ? AS s", "world") == "world" + assert "SELECT ? AS s" in cache_hits + + cache_hits.clear() + assert await aiosqlite_async_driver.select_value("SELECT ? AS b", b"bytes") == b"bytes" + assert await aiosqlite_async_driver.select_value("SELECT ? AS b", b"more") == b"more" + assert "SELECT ? AS b" in cache_hits + + cache_hits.clear() + assert await aiosqlite_async_driver.select_value("SELECT ? AS n", None) is None + assert await aiosqlite_async_driver.select_value("SELECT ? AS n", None) is None + assert "SELECT ? AS n" in cache_hits + + cache_hits.clear() + assert await aiosqlite_async_driver.select_one("SELECT ? AS a, ? AS b", 1, "two") == {"a": 1, "b": "two"} + assert await aiosqlite_async_driver.select_one("SELECT ? AS a, ? AS b", 3, "four") == {"a": 3, "b": "four"} + assert "SELECT ? AS a, ? AS b" in cache_hits + + cache_hits.clear() + r1_filtered = await aiosqlite_async_driver.execute("SELECT id, label FROM t_shapes", LimitOffsetFilter(10, 0)) + r2_filtered = await aiosqlite_async_driver.execute("SELECT id, label FROM t_shapes", LimitOffsetFilter(10, 0)) + assert r1_filtered.get_data() == r2_filtered.get_data() == [{"id": 1, "label": "one"}] + assert cache_hits == [] + + +def test_statement_cache_shared_across_sessions(tmp_path: Any, monkeypatch: Any) -> None: + """Sessions opened from the same database config share StatementCacheHolder, QueryCache, and rebind processor.""" + db_file = tmp_path / "shared_cache.db" + spec = SQLSpec() + config = SqliteConfig(connection_config={"database": str(db_file)}) + spec.add_config(config) + + try: + with spec.provide_session(config) as session1: + session1.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)") + session1.execute("INSERT INTO items (id, name) VALUES (?, ?)", (1, "alpha")) + session1.execute("INSERT INTO items (id, name) VALUES (?, ?)", (2, "beta")) + r1 = session1.select_one("SELECT id, name FROM items WHERE id = ?", 1) + assert r1 == {"id": 1, "name": "alpha"} + cache1 = session1._stmt_cache + processor1 = session1._stmt_cache_rebind_processor + holder1 = session1._cache_holder + + with spec.provide_session(config) as session2: + assert session2._stmt_cache is cache1 + assert session2._stmt_cache_rebind_processor is processor1 + assert session2._cache_holder is holder1 + + hit_calls: list[str] = [] + original_hit = type(session2)._execute_cache_hit + + def tracked_hit(self_driver: Any, sql: str, params: Any, cached: CachedQuery) -> Any: + hit_calls.append(sql) + return original_hit(self_driver, sql, params, cached) + + monkeypatch.setattr(type(session2), "_execute_cache_hit", tracked_hit) + r2 = session2.select_one("SELECT id, name FROM items WHERE id = ?", 2) + assert r2 == {"id": 2, "name": "beta"} + assert hit_calls == ["SELECT id, name FROM items WHERE id = ?"] + finally: + config.close_pool() + + +def test_statement_cache_shared_across_sessions_isolated_and_thread_safe(tmp_path: Any) -> None: + """Different configs have isolated StatementCacheHolders, standalone drivers work, and concurrent access is thread-safe.""" + spec = SQLSpec() + cfg_a = SqliteConfig(connection_config={"database": str(tmp_path / "a.db")}) + cfg_b = SqliteConfig(connection_config={"database": str(tmp_path / "b.db")}) + spec.add_config(cfg_a) + spec.add_config(cfg_b) + + try: + with spec.provide_session(cfg_a) as s_a, spec.provide_session(cfg_b) as s_b: + assert s_a._cache_holder is not s_b._cache_holder + assert s_a._stmt_cache is not s_b._stmt_cache + s_a.execute("SELECT 1 AS a") + assert s_b._stmt_cache.get("SELECT 1 AS a") is None + + errors: list[Exception] = [] + holder = s_a._cache_holder + + def worker(tid: int) -> None: + try: + for idx in range(50): + sql = f"SELECT {idx % 16} AS v" + stmt = SQL(sql, statement_config=s_a.statement_config) + holder.set_statement(sql, stmt) + _ = holder.get_statement(sql) + holder.query_cache.set(sql, _make_cached(compiled_sql=sql)) + _ = holder.query_cache.get(sql) + holder.set_row_count_hint(sql, tid + idx) + _ = holder.get_row_count_hint(sql) + holder.set_bulk_insert_sql(sql, ("t", "c")) + _ = holder.get_bulk_insert_sql(sql) + except Exception as exc: + errors.append(exc) + + threads = [threading.Thread(target=worker, args=(i,)) for i in range(4)] + for t in threads: + t.start() + for t in threads: + t.join() + assert errors == [] + finally: + cfg_a.close_pool() + cfg_b.close_pool() + + raw_conn = sqlite3.connect(":memory:") + try: + standalone = SqliteDriver(connection=raw_conn) + assert standalone._cache_holder is not None + assert standalone.select_value("SELECT ? AS v", 7) == 7 + assert standalone._stmt_cache.get("SELECT ? AS v") is not None + finally: + raw_conn.close() + + +def test_fast_path_with_observability_sync(tmp_path: Any, monkeypatch: Any) -> None: + """Fast path stays enabled with observability active and emits matching events and error hooks.""" + events: list[StatementEvent] = [] + starts: list[dict[str, Any]] = [] + completes: list[dict[str, Any]] = [] + errors: list[dict[str, Any]] = [] + + obs = ObservabilityConfig( + statement_observers=(events.append,), + lifecycle=cast( + "LifecycleConfig", + {"on_query_start": [starts.append], "on_query_complete": [completes.append], "on_error": [errors.append]}, + ), + logging=LoggingConfig(include_sql_hash=True), + ) + spec = SQLSpec() + config = SqliteConfig(connection_config={"database": str(tmp_path / "obs_sync.db")}, observability_config=obs) + spec.add_config(config) + + try: + with spec.provide_session(config) as session: + assert session._stmt_cache_enabled is True + session.execute("CREATE TABLE obs_items (id INTEGER PRIMARY KEY, label TEXT)") + + events.clear() + starts.clear() + completes.clear() + errors.clear() + + hit_calls: list[str] = [] + original_hit = type(session)._execute_cache_hit + + def tracked_hit(self_driver: Any, sql: str, params: Any, cached: CachedQuery) -> Any: + hit_calls.append(sql) + return original_hit(self_driver, sql, params, cached) + + monkeypatch.setattr(type(session), "_execute_cache_hit", tracked_hit) + + insert_sql = "INSERT INTO obs_items (id, label) VALUES (?, ?)" + session.execute(insert_sql, (1, "alpha")) + session.execute(insert_sql, (2, "beta")) + assert hit_calls == [insert_sql] + assert len(events) == 2 + assert len(starts) == 2 + assert len(completes) == 2 + + ev1, ev2 = events[0], events[1] + assert ev1.sql == ev2.sql == insert_sql + assert ev1.driver == ev2.driver == "SqliteDriver" + assert ev1.operation == ev2.operation == "INSERT" + assert ev1.execution_mode == ev2.execution_mode + assert ev1.is_many == ev2.is_many is False + assert ev1.is_script == ev2.is_script is False + assert ev1.rows_affected == ev2.rows_affected == 1 + assert ev1.sql_hash is not None + assert ev1.sql_hash == ev2.sql_hash + assert type(ev1.parameters) is type(ev2.parameters) + assert starts[0]["sql"] == starts[1]["sql"] == insert_sql + assert starts[0]["operation"] == starts[1]["operation"] == "INSERT" + assert completes[0]["rows_affected"] == completes[1]["rows_affected"] == 1 + + events.clear() + starts.clear() + completes.clear() + hit_calls.clear() + + select_sql = "SELECT id, label FROM obs_items WHERE id = ?" + r1 = session.select_one(select_sql, (1,)) + r2 = session.select_one(select_sql, (1,)) + assert r1 == r2 == {"id": 1, "label": "alpha"} + assert hit_calls == [select_sql] + assert len(events) == 2 + s_ev1, s_ev2 = events[0], events[1] + assert s_ev1.sql == s_ev2.sql == select_sql + assert s_ev1.parameters == s_ev2.parameters == [1] + assert s_ev1.driver == s_ev2.driver == "SqliteDriver" + assert s_ev1.operation == s_ev2.operation == "SELECT" + assert s_ev1.execution_mode == s_ev2.execution_mode + assert s_ev1.is_many == s_ev2.is_many is False + assert s_ev1.is_script == s_ev2.is_script is False + assert s_ev1.rows_affected == s_ev2.rows_affected == 1 + assert s_ev1.sql_hash == s_ev2.sql_hash + assert starts[0] == starts[1] + assert completes[0] == completes[1] + + with pytest.raises(SQLSpecError): + session.execute(insert_sql, (1, "duplicate")) + assert len(errors) == 1 + assert isinstance(errors[0]["exception"], SQLSpecError) + assert errors[0]["sql"] == insert_sql + + transformed_cfg = session.statement_config.replace( + statement_transformers=((lambda expr, ctx: (expr, ctx)),) + ) + transformed_drv = SqliteDriver(connection=session.connection, statement_config=transformed_cfg) + assert transformed_drv._stmt_cache_enabled is False + finally: + config.close_pool() + + +@pytest.mark.anyio +async def test_fast_path_with_observability_async(tmp_path: Any, monkeypatch: Any) -> None: + """Async fast path stays enabled with observability active and emits matching events and error hooks.""" + events: list[StatementEvent] = [] + starts: list[dict[str, Any]] = [] + completes: list[dict[str, Any]] = [] + errors: list[dict[str, Any]] = [] + + obs = ObservabilityConfig( + statement_observers=(events.append,), + lifecycle=cast( + "LifecycleConfig", + {"on_query_start": [starts.append], "on_query_complete": [completes.append], "on_error": [errors.append]}, + ), + logging=LoggingConfig(include_sql_hash=True), + ) + spec = SQLSpec() + config = AiosqliteConfig(connection_config={"database": str(tmp_path / "obs_async.db")}, observability_config=obs) + spec.add_config(config) + + try: + async with spec.provide_session(config) as session: + assert session._stmt_cache_enabled is True + await session.execute("CREATE TABLE obs_items (id INTEGER PRIMARY KEY, label TEXT)") + + events.clear() + starts.clear() + completes.clear() + errors.clear() + + hit_calls: list[str] = [] + original_hit = type(session)._execute_cache_hit + + async def tracked_hit(self_driver: Any, sql: str, params: Any, cached: CachedQuery) -> Any: + hit_calls.append(sql) + return await original_hit(self_driver, sql, params, cached) + + monkeypatch.setattr(type(session), "_execute_cache_hit", tracked_hit) + + insert_sql = "INSERT INTO obs_items (id, label) VALUES (?, ?)" + await session.execute(insert_sql, (1, "alpha")) + await session.execute(insert_sql, (2, "beta")) + assert hit_calls == [insert_sql] + assert len(events) == 2 + + select_sql = "SELECT id, label FROM obs_items WHERE id = ?" + events.clear() + starts.clear() + completes.clear() + hit_calls.clear() + + r1 = await session.select_one(select_sql, (1,)) + r2 = await session.select_one(select_sql, (1,)) + assert r1 == r2 == {"id": 1, "label": "alpha"} + assert hit_calls == [select_sql] + assert len(events) == 2 + s_ev1, s_ev2 = events[0], events[1] + assert s_ev1.sql == s_ev2.sql == select_sql + assert s_ev1.parameters == s_ev2.parameters == [1] + assert s_ev1.driver == s_ev2.driver == "AiosqliteDriver" + assert s_ev1.operation == s_ev2.operation == "SELECT" + assert s_ev1.rows_affected == s_ev2.rows_affected == 1 + assert s_ev1.sql_hash == s_ev2.sql_hash + assert starts[0] == starts[1] + assert completes[0] == completes[1] + + with pytest.raises(SQLSpecError): + await session.execute(insert_sql, (1, "duplicate")) + assert len(errors) == 1 + assert isinstance(errors[0]["exception"], SQLSpecError) + assert errors[0]["sql"] == insert_sql + finally: + await config.close_pool() + + +def test_fast_path_select_lazy_statement_materialization_sync(sqlite_sync_driver: Any) -> None: + sqlite_sync_driver._stmt_cache_enabled = True + sql = "SELECT ? AS val" + + warm_result = sqlite_sync_driver.execute(sql, (0,)) + assert warm_result.one() == {"val": 0} + assert warm_result._statement is not None + + r1 = sqlite_sync_driver.execute(sql, (1,)) + r2 = sqlite_sync_driver.execute(sql, (2,)) + assert r1._statement is None + assert r2._statement is None + assert r1.one() == {"val": 1} + assert r2.one() == {"val": 2} + assert r1._statement is None + assert r2._statement is None + + stmt1 = r1.statement + stmt2 = r2.statement + assert stmt1 is not stmt2 + assert r1._statement is stmt1 + assert r2._statement is stmt2 + assert stmt1.raw_sql == sql + assert stmt2.raw_sql == sql + assert stmt1.operation_type == "SELECT" + assert stmt2.operation_type == "SELECT" + assert stmt1._processed_state.execution_parameters == (1,) + assert stmt2._processed_state.execution_parameters == (2,) + assert stmt1.parameters == [1] + assert stmt2.parameters == [2] + + +@pytest.mark.anyio +async def test_fast_path_select_lazy_statement_materialization_async(aiosqlite_async_driver: Any) -> None: + aiosqlite_async_driver._stmt_cache_enabled = True + sql = "SELECT ? AS val" + + warm_result = await aiosqlite_async_driver.execute(sql, (0,)) + assert warm_result.one() == {"val": 0} + assert warm_result._statement is not None + + r1 = await aiosqlite_async_driver.execute(sql, (1,)) + r2 = await aiosqlite_async_driver.execute(sql, (2,)) + assert r1._statement is None + assert r2._statement is None + assert r1.one() == {"val": 1} + assert r2.one() == {"val": 2} + assert r1._statement is None + assert r2._statement is None + + stmt1 = r1.statement + stmt2 = r2.statement + assert stmt1 is not stmt2 + assert stmt1.raw_sql == sql + assert stmt2.raw_sql == sql + assert stmt1.operation_type == "SELECT" + assert stmt2.operation_type == "SELECT" + assert stmt1._processed_state.execution_parameters == (1,) + assert stmt2._processed_state.execution_parameters == (2,) + + +def test_fast_path_ambient_exception_isolation_sync(cached_sync_cursor: Any, monkeypatch: Any) -> None: + driver, _cursor, _statements, _fallback = cached_sync_cursor + sql = "SELECT ? AS value" + cached = _make_cached( + compiled_sql=sql, + param_count=1, + operation_type="SELECT", + operation_profile=OperationProfile(returns_rows=True, modifies_rows=False), + ) + + helper_calls = 0 + original_helper = driver._run_with_exception_handler + + def tracked_helper(exc_handler: Any, fn: Any, *args: Any, **kwargs: Any) -> Any: + nonlocal helper_calls + helper_calls += 1 + return original_helper(exc_handler, fn, *args, **kwargs) + + monkeypatch.setattr(driver, "_run_with_exception_handler", tracked_helper) + + res_normal = SyncDriverAdapterBase._execute_cache_hit(driver, sql, ("normal",), cached) + assert res_normal.one() == {"value": "normal"} + assert helper_calls == 0 + + try: + raise ValueError("ambient") + except ValueError: + res_ambient = SyncDriverAdapterBase._execute_cache_hit(driver, sql, ("ambient",), cached) + assert res_ambient.one() == {"value": "ambient"} + assert helper_calls == 1 + + +@pytest.mark.anyio +async def test_fast_path_ambient_exception_isolation_async(cached_async_cursor: Any, monkeypatch: Any) -> None: + driver, _cursor, _statements, _fallback = cached_async_cursor + sql = "SELECT ? AS value" + cached = _make_cached( + compiled_sql=sql, + param_count=1, + operation_type="SELECT", + operation_profile=OperationProfile(returns_rows=True, modifies_rows=False), + ) + + helper_calls = 0 + original_helper = driver._run_with_exception_handler + + async def tracked_helper(exc_handler: Any, fn: Any, *args: Any, **kwargs: Any) -> Any: + nonlocal helper_calls + helper_calls += 1 + return await original_helper(exc_handler, fn, *args, **kwargs) + + monkeypatch.setattr(driver, "_run_with_exception_handler", tracked_helper) + + res_normal = await AsyncDriverAdapterBase._execute_cache_hit(driver, sql, ("normal",), cached) + assert res_normal.one() == {"value": "normal"} + assert helper_calls == 0 + + try: + raise ValueError("ambient") + except ValueError: + res_ambient = await AsyncDriverAdapterBase._execute_cache_hit(driver, sql, ("ambient",), cached) + assert res_ambient.one() == {"value": "ambient"} + assert helper_calls == 1 + + +def test_precomputed_fast_path_coercion_and_rebind_processor(sqlite_sync_driver: Any) -> None: + coercion_map: dict[type, Any] = {int: str, float: str} + fallbacks_1 = type_coercion_fallbacks(coercion_map) + fallbacks_2 = type_coercion_fallbacks(coercion_map) + assert fallbacks_1 is fallbacks_2 + + coercion_map[bool] = int + fallbacks_3 = type_coercion_fallbacks(coercion_map) + assert fallbacks_3 is not fallbacks_1 + assert len(fallbacks_3) == 3 + + sqlite_sync_driver._stmt_cache_enabled = True + sqlite_sync_driver.execute("SELECT :val AS v", {"val": 10}) + cached_named = sqlite_sync_driver._stmt_cache.get("SELECT :val AS v") + assert cached_named is not None + assert cached_named.needs_style_remap is True + + sqlite_sync_driver.execute("SELECT ? AS v", (10,)) + cached_qmark = sqlite_sync_driver._stmt_cache.get("SELECT ? AS v") + assert cached_qmark is not None + assert cached_qmark.needs_style_remap is False + + base_stmt = SQL("SELECT * FROM users WHERE id = ?", 1, statement_config=sqlite_sync_driver.statement_config) + base_stmt.compile() + copy_1 = base_stmt.copy(parameters=(2,)) + copy_2 = base_stmt.copy(parameters=(3,)) + copy_1.compile() + copy_2.compile() + assert copy_1._rebind_processor is not None + assert copy_1._rebind_processor is copy_2._rebind_processor + + +def test_execute_many_single_pass_parameter_preparation(sqlite_sync_driver: Any) -> None: + coercion_calls = 0 + + def coerce_decimal(val: Decimal) -> str: + nonlocal coercion_calls + coercion_calls += 1 + return str(val) + + def coerce_uuid(val: UUID) -> str: + nonlocal coercion_calls + coercion_calls += 1 + return val.hex + + def coerce_date(val: date) -> str: + nonlocal coercion_calls + coercion_calls += 1 + return val.isoformat() + + custom_cfg = StatementConfig( + dialect="sqlite", + parameter_config=ParameterStyleConfig( + default_parameter_style=ParameterStyle.QMARK, + default_execution_parameter_style=ParameterStyle.QMARK, + supported_parameter_styles={ParameterStyle.QMARK, ParameterStyle.NAMED_COLON}, + supported_execution_parameter_styles={ParameterStyle.QMARK}, + type_coercion_map={Decimal: coerce_decimal, UUID: coerce_uuid, date: coerce_date}, + ), + ) + + sample_uuid = uuid4() + sample_date = date(2026, 10, 13) + tuple_rows = [ + (Decimal("10.50"), sample_uuid, sample_date, TypedParameter(7, int)), + (Decimal("20.75"), sample_uuid, sample_date, TypedParameter(8, int)), + ] + stmt_tuples = SQL("INSERT INTO items VALUES (?, ?, ?, ?)", tuple_rows, statement_config=custom_cfg, is_many=True) + stmt_tuples.compile() + _, prepared_tuples = sqlite_sync_driver._compiled_statement(stmt_tuples, custom_cfg) + assert coercion_calls == 6 + assert prepared_tuples == [("10.50", sample_uuid.hex, "2026-10-13", 7), ("20.75", sample_uuid.hex, "2026-10-13", 8)] + + coercion_calls = 0 + list_rows = [[Decimal("1.00"), sample_uuid, sample_date], [Decimal("2.00"), sample_uuid, sample_date]] + stmt_lists = SQL("INSERT INTO items VALUES (?, ?, ?)", list_rows, statement_config=custom_cfg, is_many=True) + stmt_lists.compile() + _, prepared_lists = sqlite_sync_driver._compiled_statement(stmt_lists, custom_cfg) + assert coercion_calls == 6 + assert prepared_lists == [["1.00", sample_uuid.hex, "2026-10-13"], ["2.00", sample_uuid.hex, "2026-10-13"]] + + coercion_calls = 0 + dict_rows = [{"price": Decimal("3.50"), "uid": sample_uuid}, {"price": Decimal("4.50"), "uid": sample_uuid}] + stmt_dicts = SQL("INSERT INTO items VALUES (:price, :uid)", dict_rows, statement_config=custom_cfg, is_many=True) + stmt_dicts.compile() + _, prepared_dicts = sqlite_sync_driver._compiled_statement(stmt_dicts, custom_cfg) + assert coercion_calls == 4 + assert prepared_dicts == [("3.50", sample_uuid.hex), ("4.50", sample_uuid.hex)] + + override_calls = 0 + + class CustomDriver(SqliteDriver): + def prepare_driver_parameters( + self, + parameters: Any, + statement_config: StatementConfig, + is_many: bool = False, + prepared_statement: Any | None = None, + ) -> Any: + nonlocal override_calls + override_calls += 1 + return super().prepare_driver_parameters( + parameters, statement_config, is_many=is_many, prepared_statement=prepared_statement + ) + + custom_driver = CustomDriver( + connection=sqlite_sync_driver.connection, + statement_config=custom_cfg, + driver_features=sqlite_sync_driver.driver_features, + ) + stmt_override = SQL("INSERT INTO items VALUES (?, ?, ?)", list_rows, statement_config=custom_cfg, is_many=True) + stmt_override.compile() + custom_driver._compiled_statement(stmt_override, custom_cfg) + assert override_calls == 1 + + +def test_declared_parameters_execute_many_copies_only_when_default_missing() -> None: + declarations = ( + ParameterDeclaration(name="id", type_str="int", required=True), + ParameterDeclaration(name="note", type_str="str", required=False), + ) + row_complete = {"id": 1, "note": "present"} + row_missing = {"id": 2} + stmt = SQL( + "INSERT INTO items VALUES (:id, :note)", + [row_complete, row_missing], + is_many=True, + declared_parameters=declarations, + ) + _check_declared_parameters(stmt) + rows = stmt.positional_parameters + assert rows[0] is row_complete + assert rows[1] is not row_missing + assert rows[1] == {"id": 2, "note": None} + assert "note" not in row_missing diff --git a/tests/unit/observability/test_observability_core.py b/tests/unit/observability/test_observability_core.py index baabb45fc..49bf65fec 100644 --- a/tests/unit/observability/test_observability_core.py +++ b/tests/unit/observability/test_observability_core.py @@ -21,6 +21,7 @@ from sqlspec.core import SQL, ArrowResult, StatementConfig from sqlspec.data_dictionary import ColumnMetadata, ForeignKeyMetadata, IndexMetadata, TableMetadata from sqlspec.driver import SyncDataDictionaryBase, SyncDriverAdapterBase +from sqlspec.exceptions import SQLSpecError from sqlspec.observability import ( GCPLogFormatter, LifecycleDispatcher, @@ -1026,3 +1027,38 @@ def test_observability_config_diagnostics_and_logging(caplog) -> None: truncated_batch, was_truncated = _maybe_truncate_parameters([[1, 2, 3, 4], [5, 6, 7, 8]], max_items=2) assert was_truncated is True assert truncated_batch == [[1, 2], [5, 6]] + + +def test_fast_path_with_observability_spans_and_error_recording(tmp_path: Path) -> None: + """Fast-path cached query execution records spans and span exceptions when observability is active.""" + spec = SQLSpec() + config = SqliteConfig( + connection_config={"database": str(tmp_path / "obs_spans.db")}, + observability_config=ObservabilityConfig(print_sql=True), + ) + spec.add_config(config) + + try: + with spec.provide_session(config) as session: + span_manager = _FakeSpanManager() + assert session._observability is not None + session._observability.span_manager = cast("Any", span_manager) + assert session._stmt_cache_enabled is True + + session.execute("CREATE TABLE t_span (id INTEGER PRIMARY KEY)") + span_manager.started.clear() + span_manager.finished.clear() + + session.execute("INSERT INTO t_span (id) VALUES (?)", (1,)) + session.execute("INSERT INTO t_span (id) VALUES (?)", (2,)) + assert len(span_manager.finished) == 2 + assert span_manager.finished[0].attributes["operation"] == "INSERT" + assert span_manager.finished[1].attributes["operation"] == "INSERT" + assert span_manager.finished[1].exception is None + + with pytest.raises(SQLSpecError): + session.execute("INSERT INTO t_span (id) VALUES (?)", (1,)) + assert len(span_manager.finished) == 3 + assert isinstance(span_manager.finished[2].exception, SQLSpecError) + finally: + config.close_pool() diff --git a/tests/unit/tools/test_bench_compare.py b/tests/unit/tools/test_bench_compare.py new file mode 100644 index 000000000..01409a253 --- /dev/null +++ b/tests/unit/tools/test_bench_compare.py @@ -0,0 +1,223 @@ +"""Unit tests for benchmark comparison, metadata parity, and environment collection.""" + +import json +from pathlib import Path +from typing import Any + +from click.testing import CliRunner +from tools.scripts.bench import ( + EXTENDED_SCENARIOS_BY_DRIVER, + SCENARIO_REGISTRY, + _collect_environment_metadata, + _write_json_results, +) +from tools.scripts.bench_compare import compute_comparison_rows, format_run_args, validate_metadata_parity +from tools.scripts.bench_compare import main as bench_compare_main +from tools.scripts.bench_subsystems import _build_stub_driver_benchmarks, _make_temp_db, _setup_test_table +from tools.scripts.bench_subsystems import run_benchmarks as run_subsystem_benchmarks + +__all__ = ( + "test_bench_compare_cli_exits_2_on_hard_metadata_mismatch", + "test_collect_environment_metadata_and_write_json_results", + "test_compute_comparison_rows_ratio_mode_and_fallback", + "test_extended_scenarios_registered_for_all_benchmarked_drivers", + "test_format_run_args_and_cli_print_run_args", + "test_stub_driver_microbenchmarks_run_cleanly", + "test_validate_metadata_parity_detects_hard_and_soft_mismatches", +) + + +def test_stub_driver_microbenchmarks_run_cleanly() -> None: + """All 9 stub-connection driver micro-benchmarks should build and run with positive timings.""" + stub_benches = _build_stub_driver_benchmarks(10) + expected_names = { + "stub driver: fast-path execute(SELECT, tuple)", + "stub driver: fast-path execute(INSERT, tuple)", + "stub driver: fast-path fetch_one_or_none(SELECT, tuple)", + "stub driver: parameterless execute(SELECT)", + "stub driver: scalar-param execute(SELECT, int)", + "stub driver: complex CTE+2-join execute(SELECT, int)", + "stub driver: session-per-request simple SELECT", + "stub driver: session-per-request complex CTE SELECT", + "stub driver: execute_many 100 rows", + } + assert {b.name for b in stub_benches} == expected_names + + db_path = _make_temp_db() + try: + _setup_test_table(db_path) + results = run_subsystem_benchmarks(db_path, iterations=10, warmup=2) + finally: + db_path.unlink(missing_ok=True) + + by_name = {r["name"]: r for r in results} + for name in expected_names: + assert name in by_name + assert by_name[name]["time_per_op_us"] > 0.0 + + +def test_extended_scenarios_registered_for_all_benchmarked_drivers() -> None: + """All hot-path extended scenarios should be registered for local, Postgres, and Oracle drivers.""" + expected_scenarios = ( + "scalar_param_select", + "no_param_select", + "complex_query", + "session_per_request", + "observability_select", + ) + target_drivers = ("sqlite", "aiosqlite", "duckdb", "asyncpg", "psycopg_sync", "psycopg_async", "psqlpy", "oracle") + for drv in target_drivers: + assert drv in EXTENDED_SCENARIOS_BY_DRIVER + driver_scenarios = set(EXTENDED_SCENARIOS_BY_DRIVER[drv]) + for scenario in expected_scenarios: + assert ("raw", scenario) in driver_scenarios + assert ("sqlspec", scenario) in driver_scenarios + assert callable(SCENARIO_REGISTRY["raw", drv, scenario]) + assert callable(SCENARIO_REGISTRY["sqlspec", drv, scenario]) + assert ("raw", "rows") in set(EXTENDED_SCENARIOS_BY_DRIVER["psqlpy"]) + assert ("sqlspec", "rows") in set(EXTENDED_SCENARIOS_BY_DRIVER["psqlpy"]) + + +def test_validate_metadata_parity_detects_hard_and_soft_mismatches() -> None: + """Hard metadata keys should be reported separately from soft metadata keys.""" + baseline = _sample_payload(rows=1000, pool_size=1, iterations=3, warmup=1, mypyc_compiled=False) + current = _sample_payload(rows=10000, pool_size=2, iterations=7, warmup=3, mypyc_compiled=True) + + hard_mismatches, soft_warnings = validate_metadata_parity(baseline, current) + + assert len(hard_mismatches) == 3 + assert any("rows:" in item for item in hard_mismatches) + assert any("pool_size:" in item for item in hard_mismatches) + assert any("mypyc_compiled:" in item for item in hard_mismatches) + assert len(soft_warnings) == 2 + assert any("iterations:" in item for item in soft_warnings) + assert any("warmup:" in item for item in soft_warnings) + + +def test_bench_compare_cli_exits_2_on_hard_metadata_mismatch(tmp_path: Path) -> None: + """CLI should exit with status 2 on hard metadata mismatch unless overridden.""" + base_path = tmp_path / "base.json" + curr_path = tmp_path / "curr.json" + base_path.write_text(json.dumps(_sample_payload(rows=1000)), encoding="utf-8") + curr_path.write_text(json.dumps(_sample_payload(rows=10000)), encoding="utf-8") + + runner = CliRunner() + res = runner.invoke(bench_compare_main, [str(base_path), str(curr_path)]) + assert res.exit_code == 2 + assert "Metadata mismatch" in res.output + + res_allowed = runner.invoke(bench_compare_main, [str(base_path), str(curr_path), "--allow-metadata-mismatch"]) + assert res_allowed.exit_code == 0 + + +def test_format_run_args_and_cli_print_run_args(tmp_path: Path) -> None: + """format_run_args and --print-run-args should emit matching CLI flags from baseline metadata.""" + payload = _sample_payload(rows=1000, iterations=3, pool_size=1, warmup=2) + assert format_run_args(payload) == "--rows 1000 --iterations 3 --pool-size 1 --warmup 2" + + base_path = tmp_path / "base.json" + base_path.write_text(json.dumps(payload), encoding="utf-8") + + runner = CliRunner() + res = runner.invoke(bench_compare_main, [str(base_path), "--print-run-args"]) + assert res.exit_code == 0 + assert res.output.strip() == "--rows 1000 --iterations 3 --pool-size 1 --warmup 2" + + +def test_compute_comparison_rows_ratio_mode_and_fallback() -> None: + """Ratio mode should compare sqlspec/raw ratios and fall back when raw is absent.""" + baseline = _sample_payload(raw_time=1.0, sqlspec_time=1.2) + current_same_ratio = _sample_payload(raw_time=2.0, sqlspec_time=2.4) + + rows = compute_comparison_rows(baseline, current_same_ratio, mode="ratio", threshold=5.0) + sqlspec_row = next(r for r in rows if r["library_key"] == "sqlspec") + assert sqlspec_row["metric_kind"] == "ratio" + assert abs(sqlspec_row["pct_change"]) < 1e-6 + assert sqlspec_row["status_text"] == "unchanged" + + current_regressed_ratio = _sample_payload(raw_time=1.0, sqlspec_time=1.8) + reg_rows = compute_comparison_rows(baseline, current_regressed_ratio, mode="ratio", threshold=10.0) + reg_sqlspec_row = next(r for r in reg_rows if r["library_key"] == "sqlspec") + assert reg_sqlspec_row["status_text"] == "REGRESSED" + assert reg_sqlspec_row["pct_change"] > 40.0 + + no_raw_base = _sample_payload(sqlspec_time=1.0, include_raw=False) + no_raw_curr = _sample_payload(sqlspec_time=1.5, include_raw=False) + fb_rows = compute_comparison_rows(no_raw_base, no_raw_curr, mode="ratio", threshold=10.0) + assert fb_rows[0]["metric_kind"] == "abs-fallback" + assert fb_rows[0]["status_text"] == "REGRESSED" + + +def test_collect_environment_metadata_and_write_json_results(tmp_path: Path) -> None: + """bench.py should record warmup and environment metadata in JSON output.""" + env_meta = _collect_environment_metadata() + assert "python_version" in env_meta + assert "platform" in env_meta + assert isinstance(env_meta["packages"], dict) + for pkg in ("duckdb", "sqlglot", "aiosqlite", "pyarrow", "sqlspec"): + assert pkg in env_meta["packages"] + + out_file = tmp_path / "out.json" + _write_json_results( + [ + { + "driver": "sqlite", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "initialization", + "time": 0.01, + } + ], + str(out_file), + rows=500, + pool_size=1, + iterations=3, + warmup=2, + ) + loaded = json.loads(out_file.read_text(encoding="utf-8")) + assert loaded["metadata"]["warmup"] == 2 + assert "environment" in loaded["metadata"] + assert "python_version" in loaded["metadata"]["environment"] + + +def _sample_payload( + *, + rows: int = 1000, + pool_size: int = 1, + iterations: int = 3, + warmup: int = 1, + mypyc_compiled: bool = False, + raw_time: float = 0.50, + sqlspec_time: float = 0.55, + include_raw: bool = True, +) -> dict[str, Any]: + """Create a synthetic benchmark JSON payload for comparison tests.""" + results: list[dict[str, Any]] = [] + if include_raw: + results.append({ + "driver": "duckdb", + "library": "raw", + "library_key": "raw", + "scenario": "repeated_queries", + "time": raw_time, + "stddev": 0.01, + }) + results.append({ + "driver": "duckdb", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "repeated_queries", + "time": sqlspec_time, + "stddev": 0.01, + }) + return { + "metadata": { + "rows": rows, + "pool_size": pool_size, + "iterations": iterations, + "warmup": warmup, + "mypyc_compiled": mypyc_compiled, + "timestamp": "2026-10-13T18:00:00+0000", + }, + "results": results, + } diff --git a/tools/perf_baselines/README.md b/tools/perf_baselines/README.md index bb04d65dc..d57683f2b 100644 --- a/tools/perf_baselines/README.md +++ b/tools/perf_baselines/README.md @@ -1,32 +1,74 @@ # Adapter benchmark baselines -These JSON files are produced by `tools/scripts/bench.py --extended` and are -compared by the opt-in `make bench-gate-adapters` command. They are development -and management snapshots, not an enabled CI gate: service and host conditions -can produce materially different timings. Re-capture a baseline only after a +These JSON files are produced by `tools/scripts/bench.py --extended` (and +`tools/scripts/bench_subsystems.py` for `subsystems.json`) and are compared by +the opt-in `make bench-gate-adapters` command. They are development and +management snapshots, not an enabled CI gate: service and host conditions can +produce materially different timings. Re-capture a baseline only after a deliberate performance review and run `bench_compare.py` against the previous file first. The checked-in snapshots are interpreted/uncompiled SQLSpec runs. Each file -records `metadata.mypyc_compiled: false`; no compiled-performance claim is made +records `metadata.mypyc_compiled: false`, `metadata.warmup`, and host +`metadata.environment` details (`python_version`, `platform`, `cpu_count`, +`sqlglot_version`, and `driver_versions`); no compiled-performance claim is made by these baselines. The snapshots use `pool_size: 1` so pooled SQLSpec paths are compared with the single raw connection used by the matching scenario. Use `--pool-size` only when deliberately measuring a different pool topology. -The current inventory is: +## Gate execution and comparison modes -- `sqlite.json`, `aiosqlite.json`, and `duckdb.json` for local SQLite-family - paths -- `postgres.json`, `psycopg_sync.json`, `psycopg_async.json`, and `asyncpg.json` - for PostgreSQL +- **Automatic parameter propagation**: `make bench-gate-adapters` invokes + `tools/scripts/bench_compare.py --print-run-args` before running + `tools/scripts/bench.py`, propagating `--rows`, `--iterations`, `--pool-size`, + and `--warmup` directly from the baseline's `metadata` block so fresh runs + always match the recorded baseline topology. +- **Metadata parity checks**: `bench_compare.py` validates `rows`, `pool_size`, + `iterations`, `warmup`, and `mypyc_compiled` between baseline and current JSON + files and exits with code `2` on any mismatch unless + `--allow-metadata-mismatch` is passed. +- **Ratio comparison mode (`--mode ratio`)**: `make bench-gate-adapters` + defaults to `BENCHMARK_MODE=ratio` (`--mode ratio`), which compares the + within-run `sqlspec / raw` median ratio (`current_ratio <= baseline_ratio * + (1 + max_regression_pct / 100)`) to insulate comparisons from host CPU + frequency scaling. Scenarios without a matching `raw` entry in the same JSON + file fall back to absolute median comparison (`abs-fallback`). Pass + `BENCHMARK_MODE=absolute` (or `--mode absolute`) to compare absolute median + timings across all scenarios. + +## Baseline inventory + +- `sqlite.json`, `aiosqlite.json`, and `duckdb.json` for local SQLite-family and + DuckDB paths +- `subsystems.json` for pure-Python subsystem and stub-driver hot-path + micro-benchmarks (`tools/scripts/bench_subsystems.py`) +- `postgres.json`, `psycopg_sync.json`, `psycopg_async.json`, `asyncpg.json`, + and `psqlpy.json` for PostgreSQL - `cockroach.json`, `cockroach_psycopg_sync.json`, `cockroach_psycopg_async.json`, and `cockroach_asyncpg.json` for CockroachDB - `mysql.json` and `mysqlconnector.json` for MySQL Connector/Python -- `oracle.json` for the Oracle sync and async LOB paths -- `adbc.json`, `spanner.json`, and `duckdb.json` for the other registered C8 - scenarios +- `oracle.json` for the Oracle sync and async paths +- `adbc.json` and `spanner.json` for ADBC and Cloud Spanner paths + +## Extended hot-path scenarios + +In addition to core read/write, iterative insert, repeated query, dict-key, and +schema-mapping scenarios, `--extended` captures five hot-path scenarios across +local and service-backed drivers: + +- `scalar_param_select`: repeated `SELECT` calls passing a bare scalar parameter + (`int` or `str`) rather than a wrapping tuple or list +- `no_param_select`: repeated parameterless `SELECT` queries +- `complex_query`: repeated `WITH ... JOIN ...` CTE + multi-join `SELECT` + queries exercising SQLGlot AST caching +- `session_per_request`: repeated `provide_session()` acquire/release cycles + executing a query per session +- `observability_select`: repeated `SELECT` queries with statement observers + enabled via `ObservabilityConfig` + +## Service-backed environment variables The service-backed scenarios use these environment variables: @@ -37,11 +79,13 @@ The service-backed scenarios use these environment variables: `SQLSPEC_BENCH_MYSQL_DATABASE` - `SQLSPEC_BENCH_ADBC_URI`, `SQLSPEC_BENCH_ADBC_DRIVER_NAME`, and optional ADBC username, password, backend, and TLS variables -- `SQLSPEC_BENCH_POSTGRES_DSN` for PostgreSQL psycopg and asyncpg runs -- `SQLSPEC_BENCH_COCKROACH_DSN` for CockroachDB psycopg and asyncpg runs +- `SQLSPEC_BENCH_POSTGRES_DSN` for PostgreSQL `psycopg`, `asyncpg`, and `psqlpy` + runs +- `SQLSPEC_BENCH_COCKROACH_DSN` for CockroachDB `psycopg` and `asyncpg` runs - `SQLSPEC_BENCH_ORACLE_HOST`, `SQLSPEC_BENCH_ORACLE_PORT`, `SQLSPEC_BENCH_ORACLE_SERVICE_NAME`, `SQLSPEC_BENCH_ORACLE_USER`, and - `SQLSPEC_BENCH_ORACLE_PASSWORD` for Oracle LOB runs + `SQLSPEC_BENCH_ORACLE_PASSWORD` for Oracle sync and async runs The Spanner baseline expects `sqlspec_bench_strings` to contain 1,000 rows. -The MySQL and ADBC scenarios create their benchmark tables during the run. +The MySQL, PostgreSQL, Oracle, and ADBC scenarios create their benchmark tables +during the run. diff --git a/tools/perf_baselines/adbc.json b/tools/perf_baselines/adbc.json index 1a0871280..f783cdb23 100644 --- a/tools/perf_baselines/adbc.json +++ b/tools/perf_baselines/adbc.json @@ -3,8 +3,20 @@ "rows": 1000, "pool_size": 1, "iterations": 3, + "warmup": 1, "mypyc_compiled": false, - "timestamp": "2026-07-09T20:51:07+0000" + "environment": { + "python_version": "3.11.9", + "platform": "Linux-7.1.6-1rodete1-amd64-x86_64-with-glibc2.43", + "packages": { + "duckdb": "1.5.6", + "sqlglot": "30.22.0", + "aiosqlite": "0.22.1", + "pyarrow": "26.0.0", + "sqlspec": "0.66.0" + } + }, + "timestamp": "2026-10-13T21:45:22+0000" }, "results": [ { @@ -12,15 +24,15 @@ "library": "raw", "library_key": "raw", "scenario": "rows", - "time": 0.007363428943790495, + "time": 0.6950249669898767, "times": [ - 0.0083366819890216, - 0.007363428943790495, - 0.007219261955469847 + 0.7066386119986419, + 0.6950249669898767, + 0.6572044789936626 ], - "stddev": 0.00060781482130057, - "iqr": 0.0005587100167758763, - "noise_ratio": 0.08254507865023049, + "stddev": 0.025848918638295637, + "iqr": 0.024717066502489615, + "noise_ratio": 0.037191352636217076, "noisy": false }, { @@ -28,16 +40,16 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "rows", - "time": 0.0073847780004143715, + "time": 0.18574406299740076, "times": [ - 0.0073847780004143715, - 0.007760587963275611, - 0.0070952450623735785 + 0.18574406299740076, + 0.18721129800542258, + 0.1832569239923032 ], - "stddev": 0.0003336024645634962, - "iqr": 0.0003326714504510164, - "noise_ratio": 0.045174338963849314, + "stddev": 0.0019989877786468296, + "iqr": 0.0019771870065596886, + "noise_ratio": 0.010762054767127618, "noisy": false } ] -} +} \ No newline at end of file diff --git a/tools/perf_baselines/aiosqlite.json b/tools/perf_baselines/aiosqlite.json index 2a93c7b83..4c3aa72c7 100644 --- a/tools/perf_baselines/aiosqlite.json +++ b/tools/perf_baselines/aiosqlite.json @@ -2,9 +2,21 @@ "metadata": { "rows": 1000, "pool_size": 1, - "iterations": 3, + "iterations": 5, + "warmup": 2, "mypyc_compiled": false, - "timestamp": "2026-07-09T20:51:17+0000" + "environment": { + "python_version": "3.11.9", + "platform": "Linux-7.1.6-1rodete1-amd64-x86_64-with-glibc2.43", + "packages": { + "duckdb": "1.5.6", + "sqlglot": "30.22.0", + "aiosqlite": "0.22.1", + "pyarrow": "26.0.0", + "sqlspec": "0.66.0" + } + }, + "timestamp": "2026-10-13T21:38:07+0000" }, "results": [ { @@ -12,15 +24,17 @@ "library": "raw", "library_key": "raw", "scenario": "initialization", - "time": 0.002717631054110825, + "time": 0.0029059329972369596, "times": [ - 0.002773132990114391, - 0.002717631054110825, - 0.002691857982426882 + 0.003000014999997802, + 0.003127224001218565, + 0.0029059329972369596, + 0.002840783999999985, + 0.0028367930062813684 ], - "stddev": 4.1533807512222796e-05, - "iqr": 4.063750384375453e-05, - "noise_ratio": 0.01528309276912209, + "stddev": 0.00012276764386653932, + "iqr": 0.00015923099999781698, + "noise_ratio": 0.04224723831666805, "noisy": false }, { @@ -28,31 +42,35 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "initialization", - "time": 0.002668604953214526, + "time": 0.005097148008644581, "times": [ - 0.003399751032702625, - 0.002668604953214526, - 0.0024395829532295465 + 0.005097148008644581, + 0.005182039996725507, + 0.005494557990459725, + 0.004662189996452071, + 0.004768411003169604 ], - "stddev": 0.0005014891834770644, - "iqr": 0.0004800840397365391, - "noise_ratio": 0.18792185140516385, - "noisy": true + "stddev": 0.00033412625793214127, + "iqr": 0.00041362899355590343, + "noise_ratio": 0.06555160991312692, + "noisy": false }, { "driver": "aiosqlite", "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "initialization", - "time": 0.004618455073796213, + "time": 0.004718267999123782, "times": [ - 0.004618455073796213, - 0.004851963021792471, - 0.0037743370048701763 + 0.004149066997342743, + 0.004839800007175654, + 0.006165113998576999, + 0.004497543006436899, + 0.004718267999123782 ], - "stddev": 0.0005669125606318043, - "iqr": 0.0005388130084611475, - "noise_ratio": 0.12274939380666562, + "stddev": 0.0007680568290529902, + "iqr": 0.00034225700073875487, + "noise_ratio": 0.16278363780853997, "noisy": true }, { @@ -60,15 +78,17 @@ "library": "raw", "library_key": "raw", "scenario": "write_heavy", - "time": 0.005042461096309125, + "time": 0.0046610529971076176, "times": [ - 0.004879332962445915, - 0.005042461096309125, - 0.005161065026186407 + 0.004841490997932851, + 0.0046610529971076176, + 0.004445350001333281, + 0.0052874989924021065, + 0.004659361991798505 ], - "stddev": 0.00014145119062742995, - "iqr": 0.00014086603187024593, - "noise_ratio": 0.02805201426957293, + "stddev": 0.0003170265794053061, + "iqr": 0.00018212900613434613, + "noise_ratio": 0.06801608555020393, "noisy": false }, { @@ -76,31 +96,35 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "write_heavy", - "time": 0.005528997047804296, + "time": 0.006773177010472864, "times": [ - 0.005528997047804296, - 0.0051234320271760225, - 0.00662932894192636 + 0.00658124199253507, + 0.007037850009510294, + 0.00727463599469047, + 0.006773177010472864, + 0.0067275190085638314 ], - "stddev": 0.0007792024534777046, - "iqr": 0.0007529484573751688, - "noise_ratio": 0.14093016269327643, - "noisy": true + "stddev": 0.0002759374595596812, + "iqr": 0.00031033100094646215, + "noise_ratio": 0.04073973840238037, + "noisy": false }, { "driver": "aiosqlite", "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "write_heavy", - "time": 0.010806650971062481, + "time": 0.008619442000053823, "times": [ - 0.010487912921234965, - 0.011826605070382357, - 0.010806650971062481 + 0.008513795997714624, + 0.008568481003749184, + 0.009109195991186425, + 0.008622344001196325, + 0.008619442000053823 ], - "stddev": 0.0006992849895090613, - "iqr": 0.0006693460745736957, - "noise_ratio": 0.06470876050143308, + "stddev": 0.00024033295090948416, + "iqr": 5.3862997447140515e-05, + "noise_ratio": 0.02788265770661064, "noisy": false }, { @@ -108,31 +132,35 @@ "library": "raw", "library_key": "raw", "scenario": "read_heavy", - "time": 0.006153721013106406, + "time": 0.006024611997418106, "times": [ - 0.006340676103718579, - 0.006153721013106406, - 0.005774626974016428 + 0.006055681995349005, + 0.00700543200946413, + 0.005383481999160722, + 0.00551886999164708, + 0.006024611997418106 ], - "stddev": 0.0002884083082529355, - "iqr": 0.0002830245648510754, - "noise_ratio": 0.04686730315506238, - "noisy": false + "stddev": 0.0006376032983220404, + "iqr": 0.0005368120037019253, + "noise_ratio": 0.10583308910105585, + "noisy": true }, { "driver": "aiosqlite", "library": "sqlspec", "library_key": "sqlspec", "scenario": "read_heavy", - "time": 0.0073613920249044895, + "time": 0.007915085996501148, "times": [ - 0.007489705923944712, - 0.006590571021661162, - 0.0073613920249044895 + 0.007869371998822317, + 0.007915085996501148, + 0.008159139993949793, + 0.007586259001982398, + 0.00825892599823419 ], - "stddev": 0.00048632517383475043, - "iqr": 0.00044956745114177465, - "noise_ratio": 0.06606429493083005, + "stddev": 0.00026405556292787306, + "iqr": 0.00028976799512747675, + "noise_ratio": 0.03336104788306766, "noisy": false }, { @@ -140,15 +168,17 @@ "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "read_heavy", - "time": 0.010083899949677289, + "time": 0.00960170099278912, "times": [ - 0.010083899949677289, - 0.01029131596442312, - 0.010013505001552403 + 0.00975704999291338, + 0.009748195996508002, + 0.00960170099278912, + 0.009501220993115567, + 0.008871644007740542 ], - "stddev": 0.00014442748064092192, - "iqr": 0.00013890548143535852, - "noise_ratio": 0.014322581675906451, + "stddev": 0.0003649555713030961, + "iqr": 0.0002469750033924356, + "noise_ratio": 0.03800947056955615, "noisy": false }, { @@ -156,31 +186,35 @@ "library": "raw", "library_key": "raw", "scenario": "iterative_inserts", - "time": 0.04226128803566098, + "time": 0.03316815300786402, "times": [ - 0.039598928997293115, - 0.048317581997253, - 0.04226128803566098 + 0.032772011996712536, + 0.0340141839988064, + 0.03274309399421327, + 0.03316815300786402, + 0.03575262399681378 ], - "stddev": 0.004468067485541233, - "iqr": 0.004359326499979943, - "noise_ratio": 0.1057248298199284, - "noisy": true + "stddev": 0.0012620588593239793, + "iqr": 0.0012421720020938665, + "noise_ratio": 0.038050320710494516, + "noisy": false }, { "driver": "aiosqlite", "library": "sqlspec", "library_key": "sqlspec", "scenario": "iterative_inserts", - "time": 0.15077839605510235, + "time": 0.04602402899763547, "times": [ - 0.14538006496150047, - 0.1511205150745809, - 0.15077839605510235 + 0.046554497006582096, + 0.04533095200895332, + 0.04602402899763547, + 0.04580913099925965, + 0.05507768401002977 ], - "stddev": 0.003220036012231414, - "iqr": 0.002870225056540221, - "noise_ratio": 0.021356083474018676, + "stddev": 0.0041146534881857455, + "iqr": 0.0007453660073224455, + "noise_ratio": 0.0894022878439682, "noisy": false }, { @@ -188,15 +222,17 @@ "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "iterative_inserts", - "time": 0.3442953589837998, + "time": 0.20613000499724876, "times": [ - 0.3184149160515517, - 0.3442953589837998, - 0.34637885307893157 + 0.21550685999682173, + 0.20613000499724876, + 0.20384249999187887, + 0.20573702300316654, + 0.21198704600101337 ], - "stddev": 0.015578404112130502, - "iqr": 0.013981968513689935, - "noise_ratio": 0.045247209134943685, + "stddev": 0.00490152622854084, + "iqr": 0.006250022997846827, + "noise_ratio": 0.02377881002140499, "noisy": false }, { @@ -204,31 +240,35 @@ "library": "raw", "library_key": "raw", "scenario": "repeated_queries", - "time": 0.27755877596791834, + "time": 0.1802124670066405, "times": [ - 0.2943803749512881, - 0.27755877596791834, - 0.27284023095853627 + 0.1802124670066405, + 0.21900316599931102, + 0.19717532000504434, + 0.17829404000076465, + 0.1753226220025681 ], - "stddev": 0.011322607639816268, - "iqr": 0.010770071996375918, - "noise_ratio": 0.040793549403478395, - "noisy": false + "stddev": 0.018309284325445305, + "iqr": 0.018881280004279688, + "noise_ratio": 0.1015983224111273, + "noisy": true }, { "driver": "aiosqlite", "library": "sqlspec", "library_key": "sqlspec", "scenario": "repeated_queries", - "time": 0.36091821803711355, + "time": 0.16138672298984602, "times": [ - 0.356790196034126, - 0.36091821803711355, - 0.36353744997177273 + 0.17164194499491714, + 0.16138672298984602, + 0.1611260450008558, + 0.1605991310061654, + 0.17284013899916317 ], - "stddev": 0.0034016265051633136, - "iqr": 0.0033736269688233733, - "noise_ratio": 0.009424923251764257, + "stddev": 0.006157686442135458, + "iqr": 0.010515899994061328, + "noise_ratio": 0.03815485145282293, "noisy": false }, { @@ -236,15 +276,17 @@ "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "repeated_queries", - "time": 0.45875454996712506, + "time": 0.4177528580039507, "times": [ - 0.4920030679786578, - 0.45875454996712506, - 0.4536135320086032 + 0.4177528580039507, + 0.4196554600057425, + 0.4444802340003662, + 0.402017619999242, + 0.4157857789978152 ], - "stddev": 0.0208392677718819, - "iqr": 0.019194767985027283, - "noise_ratio": 0.045425746237013384, + "stddev": 0.015374954324982959, + "iqr": 0.0038696810079272836, + "noise_ratio": 0.0368039476700302, "noisy": false }, { @@ -252,32 +294,216 @@ "library": "raw", "library_key": "raw", "scenario": "worker_hops", - "time": 0.08135498594492674, + "time": 0.06030553000164218, "times": [ - 0.08067048096563667, - 0.0959955370053649, - 0.08135498594492674 + 0.06227472001046408, + 0.05850649099738803, + 0.06533589299942832, + 0.057062860010773875, + 0.06030553000164218 ], - "stddev": 0.008657093671673539, - "iqr": 0.007662528019864112, - "noise_ratio": 0.10641134739466318, - "noisy": true + "stddev": 0.0032468092242301373, + "iqr": 0.003768229013076052, + "noise_ratio": 0.05383932823642746, + "noisy": false }, { "driver": "aiosqlite", "library": "sqlspec", "library_key": "sqlspec", "scenario": "worker_hops", - "time": 0.19313774502370507, + "time": 0.049655034992611036, + "times": [ + 0.047336261995951645, + 0.05104221199871972, + 0.049655034992611036, + 0.04931402600777801, + 0.05952784999681171 + ], + "stddev": 0.004745972212920856, + "iqr": 0.0017281859909417108, + "noise_ratio": 0.09557887158124215, + "noisy": false + }, + { + "driver": "aiosqlite", + "library": "raw", + "library_key": "raw", + "scenario": "scalar_param_select", + "time": 0.06830905401147902, + "times": [ + 0.0773702599981334, + 0.06864907201088499, + 0.05593628800124861, + 0.06162153200421017, + 0.06830905401147902 + ], + "stddev": 0.00808346849937315, + "iqr": 0.007027540006674826, + "noise_ratio": 0.11833670684437762, + "noisy": true + }, + { + "driver": "aiosqlite", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "scalar_param_select", + "time": 0.05039896701055113, + "times": [ + 0.04913739000039641, + 0.04926970601081848, + 0.05420183399110101, + 0.05039896701055113, + 0.0699791500082938 + ], + "stddev": 0.008839931812373117, + "iqr": 0.00493212798028253, + "noise_ratio": 0.17539906741585515, + "noisy": true + }, + { + "driver": "aiosqlite", + "library": "raw", + "library_key": "raw", + "scenario": "no_param_select", + "time": 0.06404038700566161, + "times": [ + 0.06529682999826036, + 0.059004966999054886, + 0.07815034200029913, + 0.06404038700566161, + 0.06270276800205465 + ], + "stddev": 0.007273892648509703, + "iqr": 0.00259406199620571, + "noise_ratio": 0.11358289649103215, + "noisy": true + }, + { + "driver": "aiosqlite", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "no_param_select", + "time": 0.04174996200890746, + "times": [ + 0.04366827699413989, + 0.04182001001026947, + 0.04174996200890746, + 0.041243350991862826, + 0.04117086100450251 + ], + "stddev": 0.0010141370775172025, + "iqr": 0.0005766590184066445, + "noise_ratio": 0.024290730547271728, + "noisy": false + }, + { + "driver": "aiosqlite", + "library": "raw", + "library_key": "raw", + "scenario": "complex_query", + "time": 0.03486508499190677, + "times": [ + 0.03542514001310337, + 0.03402129700407386, + 0.03471905700280331, + 0.03794261999428272, + 0.03486508499190677 + ], + "stddev": 0.0015096393757468506, + "iqr": 0.0007060830103000626, + "noise_ratio": 0.04329946065231974, + "noisy": false + }, + { + "driver": "aiosqlite", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "complex_query", + "time": 0.027161341000464745, + "times": [ + 0.027984411994111724, + 0.027161341000464745, + 0.026085546996910125, + 0.026560199999948964, + 0.02716834300372284 + ], + "stddev": 0.0007163733449434427, + "iqr": 0.0006081430037738755, + "noise_ratio": 0.026374741399225656, + "noisy": false + }, + { + "driver": "aiosqlite", + "library": "raw", + "library_key": "raw", + "scenario": "session_per_request", + "time": 0.04309646999172401, + "times": [ + 0.04221086901088711, + 0.042075720004504547, + 0.04317534499568865, + 0.04309646999172401, + 0.04548183400765993 + ], + "stddev": 0.0013656641765787503, + "iqr": 0.0009644759848015383, + "noise_ratio": 0.0316885391504456, + "noisy": false + }, + { + "driver": "aiosqlite", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "session_per_request", + "time": 0.04295089899096638, + "times": [ + 0.056219097998109646, + 0.042206051002722234, + 0.04096175999438856, + 0.04295089899096638, + 0.04709633499442134 + ], + "stddev": 0.006217772989444468, + "iqr": 0.004890283991699107, + "noise_ratio": 0.14476467630519718, + "noisy": true + }, + { + "driver": "aiosqlite", + "library": "raw", + "library_key": "raw", + "scenario": "observability_select", + "time": 0.0833016100077657, + "times": [ + 0.0833016100077657, + 0.0820087269967189, + 0.08787153899902478, + 0.0839166999940062, + 0.0827749780000886 + ], + "stddev": 0.0022883482606882656, + "iqr": 0.0011417219939175993, + "noise_ratio": 0.027470636647658274, + "noisy": false + }, + { + "driver": "aiosqlite", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "observability_select", + "time": 0.08435021199693438, "times": [ - 0.19621139706578106, - 0.18743419996462762, - 0.19313774502370507 + 0.08169943900429644, + 0.08435021199693438, + 0.08069263699871954, + 0.09623752698826138, + 0.0894551550009055 ], - "stddev": 0.004453780268288593, - "iqr": 0.004388598550576717, - "noise_ratio": 0.02306012358041123, + "stddev": 0.006421525646337266, + "iqr": 0.007755715996609069, + "noise_ratio": 0.07612933618436726, "noisy": false } ] -} +} \ No newline at end of file diff --git a/tools/perf_baselines/asyncpg.json b/tools/perf_baselines/asyncpg.json index a97f953fb..f2d65aa57 100644 --- a/tools/perf_baselines/asyncpg.json +++ b/tools/perf_baselines/asyncpg.json @@ -3,8 +3,20 @@ "rows": 1000, "pool_size": 1, "iterations": 3, + "warmup": 1, "mypyc_compiled": false, - "timestamp": "2026-07-09T20:52:56+0000" + "environment": { + "python_version": "3.11.9", + "platform": "Linux-7.1.6-1rodete1-amd64-x86_64-with-glibc2.43", + "packages": { + "duckdb": "1.5.6", + "sqlglot": "30.22.0", + "aiosqlite": "0.22.1", + "pyarrow": "26.0.0", + "sqlspec": "0.66.0" + } + }, + "timestamp": "2026-10-13T21:40:08+0000" }, "results": [ { @@ -12,15 +24,15 @@ "library": "raw", "library_key": "raw", "scenario": "initialization", - "time": 0.04940926597919315, + "time": 0.017716923000989482, "times": [ - 0.08479561505373567, - 0.04940926597919315, - 0.04036029998678714 + 0.025503010998363607, + 0.017716923000989482, + 0.017171163999591954 ], - "stddev": 0.023482527942621793, - "iqr": 0.022217657533474267, - "noise_ratio": 0.4752656708664843, + "stddev": 0.004660842084169627, + "iqr": 0.004165923499385826, + "noise_ratio": 0.26307288708707044, "noisy": true }, { @@ -28,47 +40,47 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "initialization", - "time": 0.07451956707518548, + "time": 0.022952098996029235, "times": [ - 0.09807045001070946, - 0.07451956707518548, - 0.043184979003854096 + 0.022945840988541022, + 0.0236565170052927, + 0.022952098996029235 ], - "stddev": 0.027534570465408085, - "iqr": 0.027442735503427684, - "noise_ratio": 0.36949450387476707, - "noisy": true + "stddev": 0.0004085144418537434, + "iqr": 0.0003553380083758384, + "noise_ratio": 0.017798565696515046, + "noisy": false }, { "driver": "asyncpg", "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "initialization", - "time": 0.06410804996266961, + "time": 0.021034752993728034, "times": [ - 0.07677872304338962, - 0.05704159999731928, - 0.06410804996266961 + 0.021034752993728034, + 0.02037326899881009, + 0.02301413299574051 ], - "stddev": 0.010000289142211531, - "iqr": 0.009868561523035169, - "noise_ratio": 0.1559911609857848, - "noisy": true + "stddev": 0.0013741463361048821, + "iqr": 0.0013204319984652102, + "noise_ratio": 0.06532742916042862, + "noisy": false }, { "driver": "asyncpg", "library": "raw", "library_key": "raw", "scenario": "write_heavy", - "time": 0.0706113880733028, + "time": 0.02644908000365831, "times": [ - 0.0719162680907175, - 0.0706113880733028, - 0.06509560893755406 + 0.02659284799301531, + 0.025781497010029852, + 0.02644908000365831 ], - "stddev": 0.003620495422310816, - "iqr": 0.0034103295765817165, - "noise_ratio": 0.051273534214513986, + "stddev": 0.00043294085541737277, + "iqr": 0.00040567549149272963, + "noise_ratio": 0.016368843655714695, "noisy": false }, { @@ -76,31 +88,31 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "write_heavy", - "time": 0.0779197090305388, + "time": 0.030817200997262262, "times": [ - 0.06527305103372782, - 0.08501916204113513, - 0.0779197090305388 + 0.030604447005316615, + 0.03229530999669805, + 0.030817200997262262 ], - "stddev": 0.010002075546728792, - "iqr": 0.009873055503703654, - "noise_ratio": 0.12836387187750808, - "noisy": true + "stddev": 0.0009209676189498793, + "iqr": 0.0008454314956907183, + "noise_ratio": 0.02988485615652428, + "noisy": false }, { "driver": "asyncpg", "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "write_heavy", - "time": 0.07024051598273218, + "time": 0.02926394098903984, "times": [ - 0.06564669101499021, - 0.07116741896606982, - 0.07024051598273218 + 0.02926394098903984, + 0.028414348009391688, + 0.03134076399146579 ], - "stddev": 0.0029563720470818433, - "iqr": 0.0027603639755398035, - "noise_ratio": 0.04208927007040542, + "stddev": 0.0015054851108868375, + "iqr": 0.0014632079910370521, + "noise_ratio": 0.05144505695424562, "noisy": false }, { @@ -108,31 +120,31 @@ "library": "raw", "library_key": "raw", "scenario": "read_heavy", - "time": 0.03612738801166415, + "time": 0.011813648990937509, "times": [ - 0.050835762987844646, - 0.03612738801166415, - 0.03557522804476321 + 0.011871584996697493, + 0.011159223009599373, + 0.011813648990937509 ], - "stddev": 0.008655683121683602, - "iqr": 0.007630267471540719, - "noise_ratio": 0.2395878472833135, - "noisy": true + "stddev": 0.0003956196714177615, + "iqr": 0.0003561809935490601, + "noise_ratio": 0.033488355013869923, + "noisy": false }, { "driver": "asyncpg", "library": "sqlspec", "library_key": "sqlspec", "scenario": "read_heavy", - "time": 0.040005053975619376, + "time": 0.016787722997833043, "times": [ - 0.040005053975619376, - 0.04019563097972423, - 0.035273249028250575 + 0.016509634006069973, + 0.01719108599354513, + 0.016787722997833043 ], - "stddev": 0.002788552244762833, - "iqr": 0.0024611909757368267, - "noise_ratio": 0.06970499893494168, + "stddev": 0.0003426397536975414, + "iqr": 0.0003407259937375784, + "noise_ratio": 0.020410138631770925, "noisy": false }, { @@ -140,15 +152,15 @@ "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "read_heavy", - "time": 0.05618906090967357, + "time": 0.015311098992242478, "times": [ - 0.06085908296518028, - 0.05618906090967357, - 0.054844368947669864 + 0.015311098992242478, + 0.014824878991930746, + 0.015347246997407638 ], - "stddev": 0.0031568467174284513, - "iqr": 0.003007357008755207, - "noise_ratio": 0.05618258547697075, + "stddev": 0.0002917147294094202, + "iqr": 0.000261184002738446, + "noise_ratio": 0.019052501035831615, "noisy": false }, { @@ -156,31 +168,31 @@ "library": "raw", "library_key": "raw", "scenario": "iterative_inserts", - "time": 0.3789590820670128, + "time": 0.7087635150091955, "times": [ - 0.4792086579836905, - 0.3789590820670128, - 0.36379395704716444 + 0.6293624990066746, + 0.7087635150091955, + 0.723250259004999 ], - "stddev": 0.06271697229167597, - "iqr": 0.05770735046826303, - "noise_ratio": 0.16549800561472092, - "noisy": true + "stddev": 0.05054585141455805, + "iqr": 0.046943879999162164, + "noise_ratio": 0.07131553803796216, + "noisy": false }, { "driver": "asyncpg", "library": "sqlspec", "library_key": "sqlspec", "scenario": "iterative_inserts", - "time": 0.47340100491419435, + "time": 0.778000054997392, "times": [ - 0.5251002670265734, - 0.47340100491419435, - 0.460795114049688 + 0.778000054997392, + 0.817270343992277, + 0.7690069039963419 ], - "stddev": 0.03407558971569178, - "iqr": 0.03215257648844272, - "noise_ratio": 0.07198039159605947, + "stddev": 0.02566577488573293, + "iqr": 0.024131719997967593, + "noise_ratio": 0.03298942554164597, "noisy": false }, { @@ -188,15 +200,15 @@ "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "iterative_inserts", - "time": 0.35054534999653697, + "time": 0.26324758399277925, "times": [ - 0.35054534999653697, - 0.34788735292386264, - 0.35071414103731513 + 0.29380316600145306, + 0.2523192360094981, + 0.26324758399277925 ], - "stddev": 0.0015855687671489063, - "iqr": 0.001413394056726247, - "noise_ratio": 0.004523148765672031, + "stddev": 0.021501894602995234, + "iqr": 0.02074196499597747, + "noise_ratio": 0.08167936159894641, "noisy": false }, { @@ -204,47 +216,47 @@ "library": "raw", "library_key": "raw", "scenario": "repeated_queries", - "time": 0.23306162597145885, + "time": 0.1380157100065844, "times": [ - 0.21710339200217277, - 0.24659188196528703, - 0.23306162597145885 + 0.16627490599057637, + 0.1380157100065844, + 0.11618138999619987 ], - "stddev": 0.014760894840657473, - "iqr": 0.01474424498155713, - "noise_ratio": 0.06333472865440799, - "noisy": false + "stddev": 0.02511533406990107, + "iqr": 0.02504675799718825, + "noise_ratio": 0.18197445833306133, + "noisy": true }, { "driver": "asyncpg", "library": "sqlspec", "library_key": "sqlspec", "scenario": "repeated_queries", - "time": 0.5471023420104757, + "time": 0.2558447600022191, "times": [ - 0.5471023420104757, - 0.6763798990286887, - 0.522445933893323 + 0.2558447600022191, + 0.2654138190118829, + 0.25346861500293016 ], - "stddev": 0.08268039956375482, - "iqr": 0.07696698256768286, - "noise_ratio": 0.15112419234018137, - "noisy": true + "stddev": 0.006323248824820633, + "iqr": 0.005972602004476357, + "noise_ratio": 0.024715178160247594, + "noisy": false }, { "driver": "asyncpg", "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "repeated_queries", - "time": 0.5908310130471364, + "time": 0.4017653000046266, "times": [ - 0.5961030019680038, - 0.5827819199766964, - 0.5908310130471364 + 0.3993142379913479, + 0.4017653000046266, + 0.4032646300038323 ], - "stddev": 0.0067086137646249185, - "iqr": 0.006660540995653719, - "noise_ratio": 0.01135453897388712, + "stddev": 0.001994212145675424, + "iqr": 0.001975196006242186, + "noise_ratio": 0.004963624647654885, "noisy": false }, { @@ -252,32 +264,192 @@ "library": "raw", "library_key": "raw", "scenario": "rows", - "time": 0.06983598996885121, + "time": 0.03334548100247048, "times": [ - 0.050157507066614926, - 0.06983598996885121, - 0.07398683298379183 + 0.031022611001390032, + 0.03566010799841024, + 0.03334548100247048 ], - "stddev": 0.012729944754628263, - "iqr": 0.011914662958588451, - "noise_ratio": 0.18228344382754755, - "noisy": true + "stddev": 0.002318749719483327, + "iqr": 0.0023187484985101037, + "noise_ratio": 0.06953715015571485, + "noisy": false }, { "driver": "asyncpg", "library": "sqlspec", "library_key": "sqlspec", "scenario": "rows", - "time": 0.06500533397775143, + "time": 0.03567721899889875, + "times": [ + 0.03413032100070268, + 0.03567721899889875, + 0.036055399003089406 + ], + "stddev": 0.0010199541531907149, + "iqr": 0.0009625390011933632, + "noise_ratio": 0.02858838726253293, + "noisy": false + }, + { + "driver": "asyncpg", + "library": "raw", + "library_key": "raw", + "scenario": "scalar_param_select", + "time": 0.055791810009395704, + "times": [ + 0.057500263006659225, + 0.05082869800389744, + 0.055791810009395704 + ], + "stddev": 0.0034655704679281752, + "iqr": 0.0033357825013808906, + "noise_ratio": 0.06211611466529855, + "noisy": false + }, + { + "driver": "asyncpg", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "scalar_param_select", + "time": 0.0876976849976927, + "times": [ + 0.0876976849976927, + 0.09220535999338608, + 0.07946519099641591 + ], + "stddev": 0.006460198622065444, + "iqr": 0.006370084498485085, + "noise_ratio": 0.07366441454224715, + "noisy": false + }, + { + "driver": "asyncpg", + "library": "raw", + "library_key": "raw", + "scenario": "no_param_select", + "time": 0.04912416500155814, + "times": [ + 0.04912416500155814, + 0.04726709499664139, + 0.059943817992461845 + ], + "stddev": 0.0068460804572092705, + "iqr": 0.006338361497910228, + "noise_ratio": 0.139362785240057, + "noisy": true + }, + { + "driver": "asyncpg", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "no_param_select", + "time": 0.07471979600086343, "times": [ - 0.06500533397775143, - 0.09050842397846282, - 0.061101199011318386 + 0.10403719200985506, + 0.06813076500839088, + 0.07471979600086343 ], - "stddev": 0.015970988018324313, - "iqr": 0.014703612483572215, - "noise_ratio": 0.24568734657667482, + "stddev": 0.019114551778593877, + "iqr": 0.01795321350073209, + "noise_ratio": 0.2558164342200961, "noisy": true + }, + { + "driver": "asyncpg", + "library": "raw", + "library_key": "raw", + "scenario": "complex_query", + "time": 0.07143511698814109, + "times": [ + 0.07541338700684719, + 0.07138984500488732, + 0.07143511698814109 + ], + "stddev": 0.0023100350695656395, + "iqr": 0.002011771000979934, + "noise_ratio": 0.03233752763293055, + "noisy": false + }, + { + "driver": "asyncpg", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "complex_query", + "time": 0.10107815499941353, + "times": [ + 0.10925904200121295, + 0.10107815499941353, + 0.09988189100113232 + ], + "stddev": 0.005103739161440163, + "iqr": 0.004688575500040315, + "noise_ratio": 0.0504929988232351, + "noisy": false + }, + { + "driver": "asyncpg", + "library": "raw", + "library_key": "raw", + "scenario": "session_per_request", + "time": 0.06375280999054667, + "times": [ + 0.06365817600453738, + 0.06375280999054667, + 0.0648492299951613 + ], + "stddev": 0.0006620299623642798, + "iqr": 0.0005955269953119569, + "noise_ratio": 0.010384326000100173, + "noisy": false + }, + { + "driver": "asyncpg", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "session_per_request", + "time": 0.19294942199485376, + "times": [ + 0.20892111099965405, + 0.19294942199485376, + 0.18774036401009653 + ], + "stddev": 0.011036705258592586, + "iqr": 0.01059037349477876, + "noise_ratio": 0.05719999129557875, + "noisy": false + }, + { + "driver": "asyncpg", + "library": "raw", + "library_key": "raw", + "scenario": "observability_select", + "time": 0.07929844300088007, + "times": [ + 0.062196694998419844, + 0.09391782700549811, + 0.07929844300088007 + ], + "stddev": 0.015876746041641212, + "iqr": 0.015860566003539134, + "noise_ratio": 0.20021510436800138, + "noisy": true + }, + { + "driver": "asyncpg", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "observability_select", + "time": 0.1119311509974068, + "times": [ + 0.12797204099479131, + 0.1119311509974068, + 0.10658306400000583 + ], + "stddev": 0.011131041605374151, + "iqr": 0.01069448849739274, + "noise_ratio": 0.09944543146556255, + "noisy": false } ] -} +} \ No newline at end of file diff --git a/tools/perf_baselines/duckdb.json b/tools/perf_baselines/duckdb.json index 72fcec951..8149ab4c0 100644 --- a/tools/perf_baselines/duckdb.json +++ b/tools/perf_baselines/duckdb.json @@ -2,9 +2,21 @@ "metadata": { "rows": 1000, "pool_size": 1, - "iterations": 3, + "iterations": 5, + "warmup": 2, "mypyc_compiled": false, - "timestamp": "2026-07-09T20:51:31+0000" + "environment": { + "python_version": "3.11.9", + "platform": "Linux-7.1.6-1rodete1-amd64-x86_64-with-glibc2.43", + "packages": { + "duckdb": "1.5.6", + "sqlglot": "30.22.0", + "aiosqlite": "0.22.1", + "pyarrow": "26.0.0", + "sqlspec": "0.66.0" + } + }, + "timestamp": "2026-10-13T21:39:11+0000" }, "results": [ { @@ -12,63 +24,71 @@ "library": "raw", "library_key": "raw", "scenario": "initialization", - "time": 0.01500460400711745, + "time": 0.017525053990539163, "times": [ - 0.01312481495551765, - 0.01500460400711745, - 0.01760098896920681 + 0.016447093003080226, + 0.018043814998236485, + 0.017525053990539163, + 0.016505255000083707, + 0.01763647500774823 ], - "stddev": 0.0022476267317144533, - "iqr": 0.00223808700684458, - "noise_ratio": 0.14979580471755796, - "noisy": true + "stddev": 0.0007163728931166758, + "iqr": 0.001131220007664524, + "noise_ratio": 0.04087707196242631, + "noisy": false }, { "driver": "duckdb", "library": "sqlspec", "library_key": "sqlspec", "scenario": "initialization", - "time": 0.015087272971868515, + "time": 0.017974551999941468, "times": [ - 0.015491381986066699, - 0.015087272971868515, - 0.014010834973305464 + 0.027214710004045628, + 0.016826530001708306, + 0.01896449898777064, + 0.017974551999941468, + 0.016810950997751206 ], - "stddev": 0.0007652932245336722, - "iqr": 0.0007402735063806176, - "noise_ratio": 0.05072442355624012, - "noisy": false + "stddev": 0.00437298426049861, + "iqr": 0.002137968986062333, + "noise_ratio": 0.24328752452427468, + "noisy": true }, { "driver": "duckdb", "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "initialization", - "time": 0.013063417980447412, + "time": 0.022122177004348487, "times": [ - 0.014519161079078913, - 0.013063417980447412, - 0.01289885607548058 + 0.016562382006668486, + 0.024464873000397347, + 0.025259608999476768, + 0.022122177004348487, + 0.020355571003165096 ], - "stddev": 0.0008917825682741047, - "iqr": 0.0008101525017991662, - "noise_ratio": 0.06826563841169858, - "noisy": false + "stddev": 0.003488630692328315, + "iqr": 0.004109301997232251, + "noise_ratio": 0.15769834459070486, + "noisy": true }, { "driver": "duckdb", "library": "raw", "library_key": "raw", "scenario": "write_heavy", - "time": 0.5354250569362193, + "time": 0.27236090099904686, "times": [ - 0.5354250569362193, - 0.5278781009837985, - 0.5440811680164188 + 0.2730083670030581, + 0.26177087699761614, + 0.2655430360027822, + 0.2809402230050182, + 0.27236090099904686 ], - "stddev": 0.008107858168099632, - "iqr": 0.008101533516310155, - "noise_ratio": 0.01514284410687461, + "stddev": 0.007403880521261014, + "iqr": 0.00746533100027591, + "noise_ratio": 0.027184079998644608, "noisy": false }, { @@ -76,47 +96,53 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "write_heavy", - "time": 0.03507585893385112, + "time": 0.0251594190049218, "times": [ - 0.03277365898247808, - 0.03507585893385112, - 0.04870448703877628 + 0.0251594190049218, + 0.02427840800373815, + 0.025719371988088824, + 0.024111591003020294, + 0.02617697800451424 ], - "stddev": 0.008610370793388203, - "iqr": 0.007965414028149098, - "noise_ratio": 0.2454785443636985, - "noisy": true + "stddev": 0.0008942051542770471, + "iqr": 0.0014409639843506739, + "noise_ratio": 0.03554156612686957, + "noisy": false }, { "driver": "duckdb", "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "write_heavy", - "time": 0.24554307002108544, + "time": 0.16889518398966175, "times": [ - 0.24554307002108544, - 0.2868850369704887, - 0.2380791320465505 + 0.16889518398966175, + 0.17277227999875322, + 0.15966579799714964, + 0.16098608399624936, + 0.16901243200118188 ], - "stddev": 0.026289684151702783, - "iqr": 0.024402952461969107, - "noise_ratio": 0.10706750611794996, - "noisy": true + "stddev": 0.00566187591403311, + "iqr": 0.008026348004932515, + "noise_ratio": 0.033523015756208176, + "noisy": false }, { "driver": "duckdb", "library": "raw", "library_key": "raw", "scenario": "read_heavy", - "time": 0.5579370099585503, + "time": 0.27635032900434453, "times": [ - 0.5456334620248526, - 0.5679827029816806, - 0.5579370099585503 + 0.28747195399773773, + 0.27950664900708944, + 0.2708783139969455, + 0.27635032900434453, + 0.2647874040121678 ], - "stddev": 0.011193612847051988, - "iqr": 0.011174620478413999, - "noise_ratio": 0.020062502840389764, + "stddev": 0.008599082462383771, + "iqr": 0.008628335010143928, + "noise_ratio": 0.031116599330151637, "noisy": false }, { @@ -124,31 +150,35 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "read_heavy", - "time": 0.02181598509196192, + "time": 0.02813689700269606, "times": [ - 0.020501775084994733, - 0.02181598509196192, - 0.02576112002134323 + 0.02916548799839802, + 0.03059967099397909, + 0.02813689700269606, + 0.027920551001443528, + 0.026502395994612016 ], - "stddev": 0.002737150181999877, - "iqr": 0.002629672468174249, - "noise_ratio": 0.125465348938489, - "noisy": true + "stddev": 0.0015250733145710964, + "iqr": 0.0012449369969544932, + "noise_ratio": 0.054201901312179696, + "noisy": false }, { "driver": "duckdb", "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "read_heavy", - "time": 0.14476253802422434, + "time": 0.1688470560038695, "times": [ - 0.14476253802422434, - 0.1383658549748361, - 0.1516585439676419 + 0.16352912900038064, + 0.18909141200128943, + 0.1688470560038695, + 0.1749409580079373, + 0.16221145800955128 ], - "stddev": 0.006647907348249282, - "iqr": 0.0066463444964028895, - "noise_ratio": 0.045922843292073474, + "stddev": 0.010930427610856013, + "iqr": 0.011411829007556662, + "noise_ratio": 0.06473567185326297, "noisy": false }, { @@ -156,15 +186,17 @@ "library": "raw", "library_key": "raw", "scenario": "iterative_inserts", - "time": 0.728288913029246, + "time": 0.4838850100059062, "times": [ - 0.7497461149469018, - 0.728288913029246, - 0.7237348459893838 + 0.48746505301096477, + 0.5004024140071124, + 0.4831144710042281, + 0.4838850100059062, + 0.4807628340058727 ], - "stddev": 0.013890867224660907, - "iqr": 0.01300563447875902, - "noise_ratio": 0.01907329217313389, + "stddev": 0.0078016727103478395, + "iqr": 0.0043505820067366585, + "noise_ratio": 0.016122989034631625, "noisy": false }, { @@ -172,15 +204,17 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "iterative_inserts", - "time": 0.7646942570572719, + "time": 0.5211548799998127, "times": [ - 0.7265271419892088, - 0.7646942570572719, - 0.7901537410216406 + 0.5169741930003511, + 0.529034426988801, + 0.515003958993475, + 0.5343208289996255, + 0.5211548799998127 ], - "stddev": 0.03202410056188041, - "iqr": 0.03181329951621592, - "noise_ratio": 0.041878306612523654, + "stddev": 0.00818205303833578, + "iqr": 0.012060233988449909, + "noise_ratio": 0.01569984922397488, "noisy": false }, { @@ -188,31 +222,35 @@ "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "iterative_inserts", - "time": 0.4311121969949454, + "time": 0.5087755779968575, "times": [ - 0.37307053699623793, - 0.46172096696682274, - 0.4311121969949454 + 0.5150670680013718, + 0.5108952939917799, + 0.4977407469996251, + 0.5087755779968575, + 0.48492712900042534 ], - "stddev": 0.04502708412456963, - "iqr": 0.044325214985292405, - "noise_ratio": 0.10444400422541872, - "noisy": true + "stddev": 0.012190548764696415, + "iqr": 0.013154546992154792, + "noise_ratio": 0.02396056196858512, + "noisy": false }, { "driver": "duckdb", "library": "raw", "library_key": "raw", "scenario": "repeated_queries", - "time": 0.8126708290074021, + "time": 0.5912800810037879, "times": [ - 0.8529211860150099, - 0.8126708290074021, - 0.8031523550162092 + 0.5912800810037879, + 0.5926464279909851, + 0.5968218740017619, + 0.5694752499985043, + 0.5844443969981512 ], - "stddev": 0.026418519783629323, - "iqr": 0.024884415499400347, - "noise_ratio": 0.032508266373849, + "stddev": 0.010727259434074375, + "iqr": 0.008202030992833897, + "noise_ratio": 0.018142433304810843, "noisy": false }, { @@ -220,15 +258,17 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "repeated_queries", - "time": 0.31618001603055745, + "time": 0.40210071598994546, "times": [ - 0.3080287959892303, - 0.32897085207514465, - 0.31618001603055745 + 0.40191533899633214, + 0.40210071598994546, + 0.38879104499937966, + 0.4024814529984724, + 0.40937379500246607 ], - "stddev": 0.010556337653965887, - "iqr": 0.010471028042957187, - "noise_ratio": 0.033387112147358684, + "stddev": 0.007473291566128634, + "iqr": 0.0005661140021402389, + "noise_ratio": 0.018585621136559986, "noisy": false }, { @@ -236,15 +276,17 @@ "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "repeated_queries", - "time": 0.49589174101129174, + "time": 0.6077469360025134, "times": [ - 0.5090508409775794, - 0.4797938499832526, - 0.49589174101129174 + 0.6077469360025134, + 0.5911962840036722, + 0.6038923649903154, + 0.6194470179907512, + 0.6114226699864957 ], - "stddev": 0.01465307436180483, - "iqr": 0.014628495497163385, - "noise_ratio": 0.029548938104760995, + "stddev": 0.010417842280132775, + "iqr": 0.007530304996180348, + "noise_ratio": 0.017141743813069082, "noisy": false }, { @@ -252,15 +294,17 @@ "library": "raw", "library_key": "raw", "scenario": "bulk", - "time": 0.19508839305490255, + "time": 0.22836395900230855, "times": [ - 0.20273231191094965, - 0.19508839305490255, - 0.1872070439858362 + 0.24037164299807046, + 0.23213120900618378, + 0.22567248198902234, + 0.22836395900230855, + 0.22686319999047555 ], - "stddev": 0.007762936544599403, - "iqr": 0.00776263396255672, - "noise_ratio": 0.03979189342348382, + "stddev": 0.005938038295275641, + "iqr": 0.0052680090157082304, + "noise_ratio": 0.02600251949220942, "noisy": false }, { @@ -268,16 +312,198 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "bulk", - "time": 0.022142705973237753, + "time": 0.028122086005168967, + "times": [ + 0.0282050310052, + 0.02779389900388196, + 0.029596199994557537, + 0.028122086005168967, + 0.02653350900800433 + ], + "stddev": 0.001093766196653912, + "iqr": 0.0004111320013180375, + "noise_ratio": 0.03889349447451632, + "noisy": false + }, + { + "driver": "duckdb", + "library": "raw", + "library_key": "raw", + "scenario": "scalar_param_select", + "time": 0.21619637300318573, + "times": [ + 0.20428137898852583, + 0.21619637300318573, + 0.21665521399700083, + 0.21974622500420082, + 0.2149981660040794 + ], + "stddev": 0.00590839135077033, + "iqr": 0.0016570479929214343, + "noise_ratio": 0.02732881809577475, + "noisy": false + }, + { + "driver": "duckdb", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "scalar_param_select", + "time": 0.2635343500005547, + "times": [ + 0.2635343500005547, + 0.2685508900030982, + 0.26267723699857015, + 0.2684782959986478, + 0.261440753005445 + ], + "stddev": 0.0033503236702337987, + "iqr": 0.005801059000077657, + "noise_ratio": 0.012713043556662525, + "noisy": false + }, + { + "driver": "duckdb", + "library": "raw", + "library_key": "raw", + "scenario": "no_param_select", + "time": 0.19317925500217825, + "times": [ + 0.1998950720007997, + 0.20632363799086306, + 0.19317925500217825, + 0.18973850399197545, + 0.18192282700329088 + ], + "stddev": 0.00936871901324091, + "iqr": 0.010156568008824252, + "noise_ratio": 0.0484975418977326, + "noisy": false + }, + { + "driver": "duckdb", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "no_param_select", + "time": 0.22625853099452797, + "times": [ + 0.22108720098913182, + 0.22625853099452797, + 0.2388700820010854, + 0.22014602599665523, + 0.22822348900081124 + ], + "stddev": 0.007497488074424063, + "iqr": 0.007136288011679426, + "noise_ratio": 0.03313681937856031, + "noisy": false + }, + { + "driver": "duckdb", + "library": "raw", + "library_key": "raw", + "scenario": "complex_query", + "time": 0.7410493529896485, + "times": [ + 0.7508263810013887, + 0.7244813430006616, + 0.7409897610050393, + 0.7410493529896485, + 0.7442528079991462 + ], + "stddev": 0.009717183383181274, + "iqr": 0.003263046994106844, + "noise_ratio": 0.013112734454161262, + "noisy": false + }, + { + "driver": "duckdb", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "complex_query", + "time": 0.785038005997194, + "times": [ + 0.7873634440038586, + 0.785038005997194, + 0.7939347870124038, + 0.7677251519926358, + 0.7666407360084122 + ], + "stddev": 0.012276230418988668, + "iqr": 0.019638292011222802, + "noise_ratio": 0.015637752981646786, + "noisy": false + }, + { + "driver": "duckdb", + "library": "raw", + "library_key": "raw", + "scenario": "session_per_request", + "time": 0.19623292100732215, + "times": [ + 0.19154544899356551, + 0.19623292100732215, + 0.19550153099407908, + 0.20088464800210204, + 0.20162821499980055 + ], + "stddev": 0.004152442517909684, + "iqr": 0.005383117008022964, + "noise_ratio": 0.02116078431994977, + "noisy": false + }, + { + "driver": "duckdb", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "session_per_request", + "time": 0.2186708630033536, + "times": [ + 0.23873932000424247, + 0.2186708630033536, + 0.21853418899991084, + 0.21829681300732773, + 0.22194015799323097 + ], + "stddev": 0.008794532505014267, + "iqr": 0.00340596899332013, + "noise_ratio": 0.04021812684243804, + "noisy": false + }, + { + "driver": "duckdb", + "library": "raw", + "library_key": "raw", + "scenario": "observability_select", + "time": 0.340502565013594, + "times": [ + 0.3527091509895399, + 0.340502565013594, + 0.3416743720008526, + 0.33067003199539613, + 0.32623431300453376 + ], + "stddev": 0.01033974921645219, + "iqr": 0.01100434000545647, + "noise_ratio": 0.03036614192917869, + "noisy": false + }, + { + "driver": "duckdb", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "observability_select", + "time": 0.4111040999996476, "times": [ - 0.022142705973237753, - 0.023443711921572685, - 0.02160847804043442 + 0.40193469599762466, + 0.4111040999996476, + 0.4252187140082242, + 0.4178961379948305, + 0.41012621400295757 ], - "stddev": 0.0009439367351918612, - "iqr": 0.0009176169405691326, - "noise_ratio": 0.0426296919776981, + "stddev": 0.00876406819580416, + "iqr": 0.007769923991872929, + "noise_ratio": 0.021318367284129912, "noisy": false } ] -} +} \ No newline at end of file diff --git a/tools/perf_baselines/oracle.json b/tools/perf_baselines/oracle.json index 1a590b631..45ec58da6 100644 --- a/tools/perf_baselines/oracle.json +++ b/tools/perf_baselines/oracle.json @@ -3,24 +3,84 @@ "rows": 1000, "pool_size": 1, "iterations": 3, + "warmup": 1, "mypyc_compiled": false, - "timestamp": "2026-07-09T20:52:45+0000" + "environment": { + "python_version": "3.11.9", + "platform": "Linux-7.1.6-1rodete1-amd64-x86_64-with-glibc2.43", + "packages": { + "duckdb": "1.5.6", + "sqlglot": "30.22.0", + "aiosqlite": "0.22.1", + "pyarrow": "26.0.0", + "sqlspec": "0.66.0" + } + }, + "timestamp": "2026-10-13T21:45:49+0000" }, "results": [ + { + "driver": "oracle", + "library": "sqlspec_native_json", + "library_key": "sqlspec_native_json", + "scenario": "json_write", + "time": 0.07864046999020502, + "times": [ + 0.08248624700354412, + 0.07840510499954689, + 0.07864046999020502 + ], + "stddev": 0.0022913284928161013, + "iqr": 0.0020405710019986145, + "noise_ratio": 0.029136759903666588, + "noisy": false + }, + { + "driver": "oracle", + "library": "sqlspec_serialized_json", + "library_key": "sqlspec_serialized_json", + "scenario": "json_write", + "time": 0.07814407799742185, + "times": [ + 0.07814407799742185, + 0.07612711799447425, + 0.07842388199060224 + ], + "stddev": 0.0012530990105866801, + "iqr": 0.0011483819980639964, + "noise_ratio": 0.016035751431196395, + "noisy": false + }, + { + "driver": "oracle", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "json_read", + "time": 0.0880015520087909, + "times": [ + 0.08080838101159316, + 0.0880015520087909, + 0.08822829400014598 + ], + "stddev": 0.004219957142026944, + "iqr": 0.003709956494276412, + "noise_ratio": 0.04795321270703717, + "noisy": false + }, { "driver": "oracle", "library": "raw", "library_key": "raw", "scenario": "lob_fetch_1k", - "time": 0.11334440193604678, + "time": 0.13012522499775514, "times": [ - 0.09634258400183171, - 0.11334440193604678, - 0.12572037801146507 + 0.2275023889960721, + 0.1296148249966791, + 0.13012522499775514 ], - "stddev": 0.014749471051306513, - "iqr": 0.014688897004816681, - "noise_ratio": 0.13012968262542624, + "stddev": 0.05636864932781265, + "iqr": 0.04894378199969651, + "noise_ratio": 0.43318771843649145, "noisy": true }, { @@ -28,15 +88,15 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "lob_fetch_1k", - "time": 0.10900005104485899, + "time": 0.13817656799801625, "times": [ - 0.11071688507217914, - 0.09607100102584809, - 0.10900005104485899 + 0.13817656799801625, + 0.13850666300277226, + 0.13262061899877153 ], - "stddev": 0.008006349282184572, - "iqr": 0.007322942023165524, - "noise_ratio": 0.07345271131010349, + "stddev": 0.0033071399025796228, + "iqr": 0.0029430220020003617, + "noise_ratio": 0.023934158667387816, "noisy": false }, { @@ -44,15 +104,15 @@ "library": "sqlspec fetch_lobs=True", "library_key": "sqlspec_fetch_lobs_true", "scenario": "lob_fetch_1k", - "time": 0.16698699200060219, + "time": 0.13564415200380608, "times": [ - 0.13185496896039695, - 0.1696973090292886, - 0.16698699200060219 + 0.13564415200380608, + 0.09577904200705234, + 0.13576182699762285 ], - "stddev": 0.021109427448350895, - "iqr": 0.018921170034445822, - "noise_ratio": 0.1264136038109769, + "stddev": 0.023050176926506932, + "iqr": 0.019991392495285254, + "noise_ratio": 0.1699312250915186, "noisy": true }, { @@ -60,31 +120,31 @@ "library": "sqlspec async", "library_key": "sqlspec_async", "scenario": "lob_fetch_1k", - "time": 0.09288392309099436, + "time": 0.11513849400216714, "times": [ - 0.09288392309099436, - 0.08804141101427376, - 0.10262110095936805 + 0.13331291699432768, + 0.11513849400216714, + 0.09306900200317614 ], - "stddev": 0.007425518107048319, - "iqr": 0.007289844972547144, - "noise_ratio": 0.07994406200709094, - "noisy": false + "stddev": 0.020153348861527264, + "iqr": 0.02012195749557577, + "noise_ratio": 0.1750357170830108, + "noisy": true }, { "driver": "oracle", "library": "sqlspec async fetch_lobs=True", "library_key": "sqlspec_async_fetch_lobs_true", "scenario": "lob_fetch_1k", - "time": 0.12941001495346427, + "time": 0.1106022239982849, "times": [ - 0.11710295605007559, - 0.1819777189521119, - 0.12941001495346427 + 0.10332141499384306, + 0.14190397401398513, + 0.1106022239982849 ], - "stddev": 0.03445664441037059, - "iqr": 0.032437381451018155, - "noise_ratio": 0.2662594886706502, + "stddev": 0.020499689405562942, + "iqr": 0.019291279510071035, + "noise_ratio": 0.18534608676477274, "noisy": true }, { @@ -92,63 +152,63 @@ "library": "raw", "library_key": "raw", "scenario": "lob_fetch_100k", - "time": 0.3411060890648514, + "time": 0.42109400998742785, "times": [ - 0.3130992379738018, - 0.3942504409933463, - 0.3411060890648514 + 0.42109400998742785, + 0.4212912819930352, + 0.3734991649980657 ], - "stddev": 0.04121937974920549, - "iqr": 0.04057560150977224, - "noise_ratio": 0.12084035164018671, - "noisy": true + "stddev": 0.027536020750604916, + "iqr": 0.023896058497484773, + "noise_ratio": 0.06539162300462795, + "noisy": false }, { "driver": "oracle", "library": "sqlspec", "library_key": "sqlspec", "scenario": "lob_fetch_100k", - "time": 0.4587748399935663, + "time": 0.5610299410036532, "times": [ - 0.4587748399935663, - 0.7968855670187622, - 0.4368935209931806 + 0.5615002539998386, + 0.5610299410036532, + 0.497147580012097 ], - "stddev": 0.2018216741963012, - "iqr": 0.17999602301279083, - "noise_ratio": 0.4399144342770225, - "noisy": true + "stddev": 0.03701901288424286, + "iqr": 0.03217633699387079, + "noise_ratio": 0.06598402362985795, + "noisy": false }, { "driver": "oracle", "library": "sqlspec fetch_lobs=True", "library_key": "sqlspec_fetch_lobs_true", "scenario": "lob_fetch_100k", - "time": 0.4545840279897675, + "time": 0.509552031988278, "times": [ - 0.5374515409348533, - 0.4545840279897675, - 0.412251268979162 + 0.509552031988278, + 0.535218512013671, + 0.48775234499771614 ], - "stddev": 0.06368437182037287, - "iqr": 0.06260013597784564, - "noise_ratio": 0.14009372942994464, - "noisy": true + "stddev": 0.023759319438088882, + "iqr": 0.023733083507977426, + "noise_ratio": 0.046627857307093325, + "noisy": false }, { "driver": "oracle", "library": "sqlspec async", "library_key": "sqlspec_async", "scenario": "lob_fetch_100k", - "time": 0.37876010907348245, + "time": 0.49131389398826286, "times": [ - 0.37467916298191994, - 0.37876010907348245, - 0.42228385794442147 + 0.46747367799980566, + 0.49131389398826286, + 0.5094287620013347 ], - "stddev": 0.026385532081223424, - "iqr": 0.023802347481250763, - "noise_ratio": 0.06966291182502649, + "stddev": 0.02104254981017692, + "iqr": 0.020977542000764515, + "noise_ratio": 0.042829136459714305, "noisy": false }, { @@ -156,16 +216,256 @@ "library": "sqlspec async fetch_lobs=True", "library_key": "sqlspec_async_fetch_lobs_true", "scenario": "lob_fetch_100k", - "time": 0.3802608890691772, + "time": 0.5330417540099006, + "times": [ + 0.5330417540099006, + 0.4851491269946564, + 0.6489256560016656 + ], + "stddev": 0.08420761437942338, + "iqr": 0.08188826450350462, + "noise_ratio": 0.15797564402029812, + "noisy": true + }, + { + "driver": "oracle", + "library": "raw", + "library_key": "raw", + "scenario": "scalar_param_select", + "time": 0.06203392099996563, + "times": [ + 0.06339251199096907, + 0.06203392099996563, + 0.06139897400862537 + ], + "stddev": 0.00101842368800144, + "iqr": 0.000996768991171848, + "noise_ratio": 0.01641720645067727, + "noisy": false + }, + { + "driver": "oracle", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "scalar_param_select", + "time": 0.1307851369929267, + "times": [ + 0.14107338000030722, + 0.12638328499451745, + 0.1307851369929267 + ], + "stddev": 0.007539044330173677, + "iqr": 0.007345047502894886, + "noise_ratio": 0.05764450382906594, + "noisy": false + }, + { + "driver": "oracle", + "library": "sqlspec async", + "library_key": "sqlspec_async", + "scenario": "scalar_param_select", + "time": 0.1558769819966983, + "times": [ + 0.1558769819966983, + 0.14717672500410117, + 0.16428799300047103 + ], + "stddev": 0.008556041435601831, + "iqr": 0.00855563399818493, + "noise_ratio": 0.054889704214205665, + "noisy": false + }, + { + "driver": "oracle", + "library": "raw", + "library_key": "raw", + "scenario": "no_param_select", + "time": 0.06161634100135416, + "times": [ + 0.06161634100135416, + 0.06202088299323805, + 0.0611521960090613 + ], + "stddev": 0.00043468415229643367, + "iqr": 0.00043434349208837375, + "noise_ratio": 0.007054689474126361, + "noisy": false + }, + { + "driver": "oracle", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "no_param_select", + "time": 0.09843701800855342, + "times": [ + 0.09812828799476847, + 0.09843701800855342, + 0.1039000619930448 + ], + "stddev": 0.003246884118789321, + "iqr": 0.0028858869991381653, + "noise_ratio": 0.03298438112486495, + "noisy": false + }, + { + "driver": "oracle", + "library": "sqlspec async", + "library_key": "sqlspec_async", + "scenario": "no_param_select", + "time": 0.11800301499897614, + "times": [ + 0.12098435100051574, + 0.11800301499897614, + 0.11768404199392535 + ], + "stddev": 0.0018203547122749167, + "iqr": 0.001650154503295198, + "noise_ratio": 0.015426340693843382, + "noisy": false + }, + { + "driver": "oracle", + "library": "raw", + "library_key": "raw", + "scenario": "complex_query", + "time": 0.17137710101087578, + "times": [ + 0.17137710101087578, + 0.17271279399574269, + 0.1611639339971589 + ], + "stddev": 0.006317555216310423, + "iqr": 0.005774429999291897, + "noise_ratio": 0.036863473469010914, + "noisy": false + }, + { + "driver": "oracle", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "complex_query", + "time": 0.2389109599898802, + "times": [ + 0.2389109599898802, + 0.2558247559936717, + 0.2387292249914026 + ], + "stddev": 0.009818067555253171, + "iqr": 0.008547765501134563, + "noise_ratio": 0.041095090638240475, + "noisy": false + }, + { + "driver": "oracle", + "library": "sqlspec async", + "library_key": "sqlspec_async", + "scenario": "complex_query", + "time": 0.2694799119926756, + "times": [ + 0.2694799119926756, + 0.292193061002763, + 0.2636004509986378 + ], + "stddev": 0.015099627734830876, + "iqr": 0.014296305002062581, + "noise_ratio": 0.05603247983560748, + "noisy": false + }, + { + "driver": "oracle", + "library": "raw", + "library_key": "raw", + "scenario": "session_per_request", + "time": 0.09713813700363971, + "times": [ + 0.09713813700363971, + 0.0955697499884991, + 0.10476380900945514 + ], + "stddev": 0.004918357324865727, + "iqr": 0.0045970295104780234, + "noise_ratio": 0.05063260915413108, + "noisy": false + }, + { + "driver": "oracle", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "session_per_request", + "time": 0.25495219499862287, + "times": [ + 0.25495219499862287, + 0.24848666999605484, + 0.2949687799991807 + ], + "stddev": 0.025178418798011637, + "iqr": 0.023241055001562927, + "noise_ratio": 0.0987574113576376, + "noisy": false + }, + { + "driver": "oracle", + "library": "sqlspec async", + "library_key": "sqlspec_async", + "scenario": "session_per_request", + "time": 0.32401383300020825, + "times": [ + 0.32401383300020825, + 0.3523620129999472, + 0.30351131899806205 + ], + "stddev": 0.024530126695458978, + "iqr": 0.024425347000942566, + "noise_ratio": 0.07570703530871539, + "noisy": false + }, + { + "driver": "oracle", + "library": "raw", + "library_key": "raw", + "scenario": "observability_select", + "time": 0.10233181800867897, + "times": [ + 0.10233181800867897, + 0.09745152399409562, + 0.11289458000101149 + ], + "stddev": 0.007893849814973921, + "iqr": 0.007721528003457934, + "noise_ratio": 0.07713973980511543, + "noisy": false + }, + { + "driver": "oracle", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "observability_select", + "time": 0.20812175900209695, + "times": [ + 0.18384120099653956, + 0.20812175900209695, + 0.21966595099365804 + ], + "stddev": 0.018285816808642073, + "iqr": 0.017912374998559244, + "noise_ratio": 0.08786114866758277, + "noisy": false + }, + { + "driver": "oracle", + "library": "sqlspec async", + "library_key": "sqlspec_async", + "scenario": "observability_select", + "time": 0.2116879549867008, "times": [ - 0.43898606300354004, - 0.3802608890691772, - 0.3690725869964808 + 0.21655518499028403, + 0.21100878600555006, + 0.2116879549867008 ], - "stddev": 0.03755377984588838, - "iqr": 0.03495673800352961, - "noise_ratio": 0.09875793415887318, + "stddev": 0.0030252751971403713, + "iqr": 0.0027731994923669845, + "noise_ratio": 0.014291201392778502, "noisy": false } ] -} +} \ No newline at end of file diff --git a/tools/perf_baselines/postgres.json b/tools/perf_baselines/postgres.json index bf3331f01..79d2afb68 100644 --- a/tools/perf_baselines/postgres.json +++ b/tools/perf_baselines/postgres.json @@ -3,8 +3,20 @@ "rows": 1000, "pool_size": 1, "iterations": 3, + "warmup": 1, "mypyc_compiled": false, - "timestamp": "2026-07-09T20:53:14+0000" + "environment": { + "python_version": "3.11.9", + "platform": "Linux-7.1.6-1rodete1-amd64-x86_64-with-glibc2.43", + "packages": { + "duckdb": "1.5.6", + "sqlglot": "30.22.0", + "aiosqlite": "0.22.1", + "pyarrow": "26.0.0", + "sqlspec": "0.66.0" + } + }, + "timestamp": "2026-10-13T21:43:07+0000" }, "results": [ { @@ -12,47 +24,207 @@ "library": "raw", "library_key": "raw", "scenario": "rows", - "time": 0.04123174399137497, + "time": 0.030046815998503007, "times": [ - 0.04123174399137497, - 0.04227600106969476, - 0.03454155509825796 + 0.03310013300506398, + 0.030046815998503007, + 0.028812962002120912 ], - "stddev": 0.0041966406159427505, - "iqr": 0.0038672229857183993, - "noise_ratio": 0.10178178776092085, - "noisy": true + "stddev": 0.0022069955307090533, + "iqr": 0.0021435855014715344, + "noise_ratio": 0.07345189356566134, + "noisy": false }, { "driver": "psycopg_sync", "library": "sqlspec", "library_key": "sqlspec", "scenario": "rows", - "time": 0.05376478098332882, + "time": 0.03645020199473947, + "times": [ + 0.03645020199473947, + 0.03639265400124714, + 0.0367621240002336 + ], + "stddev": 0.00019879436159740794, + "iqr": 0.00018473499949323013, + "noise_ratio": 0.005453861726914384, + "noisy": false + }, + { + "driver": "psycopg_sync", + "library": "raw", + "library_key": "raw", + "scenario": "scalar_param_select", + "time": 0.044167046988150105, + "times": [ + 0.04452098500041757, + 0.044167046988150105, + 0.043153007994988 + ], + "stddev": 0.0007100361825950722, + "iqr": 0.000683988502714783, + "noise_ratio": 0.016076152494088475, + "noisy": false + }, + { + "driver": "psycopg_sync", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "scalar_param_select", + "time": 0.07245179500023369, + "times": [ + 0.07242586999200284, + 0.07245179500023369, + 0.0825482079962967 + ], + "stddev": 0.005836665060436103, + "iqr": 0.005061169002146926, + "noise_ratio": 0.08055928856444863, + "noisy": false + }, + { + "driver": "psycopg_sync", + "library": "raw", + "library_key": "raw", + "scenario": "no_param_select", + "time": 0.047224439011188224, + "times": [ + 0.04150796199974138, + 0.047224439011188224, + 0.049500175009598024 + ], + "stddev": 0.004117696616470997, + "iqr": 0.003996106504928321, + "noise_ratio": 0.08719418806638336, + "noisy": false + }, + { + "driver": "psycopg_sync", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "no_param_select", + "time": 0.07078680999984499, + "times": [ + 0.07078680999984499, + 0.07032083200465422, + 0.07429387599404436 + ], + "stddev": 0.002171855041253163, + "iqr": 0.0019865219946950674, + "noise_ratio": 0.030681634633032893, + "noisy": false + }, + { + "driver": "psycopg_sync", + "library": "raw", + "library_key": "raw", + "scenario": "complex_query", + "time": 0.06520880499738269, "times": [ - 0.044566972996108234, - 0.05376478098332882, - 0.05916502000764012 + 0.06321685999864712, + 0.06520880499738269, + 0.0810561050020624 ], - "stddev": 0.007380890075507703, - "iqr": 0.007299023505765945, - "noise_ratio": 0.13728113349510976, + "stddev": 0.00977533822610234, + "iqr": 0.008919622501707636, + "noise_ratio": 0.1499082558941955, "noisy": true }, + { + "driver": "psycopg_sync", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "complex_query", + "time": 0.09353936200204771, + "times": [ + 0.09323644400865305, + 0.09353936200204771, + 0.09428284800378606 + ], + "stddev": 0.0005384378742457099, + "iqr": 0.0005232019975665025, + "noise_ratio": 0.005756270544521383, + "noisy": false + }, + { + "driver": "psycopg_sync", + "library": "raw", + "library_key": "raw", + "scenario": "session_per_request", + "time": 0.05504183200537227, + "times": [ + 0.05480232399713714, + 0.06185186799848452, + 0.05504183200537227 + ], + "stddev": 0.0040027079363106595, + "iqr": 0.0035247720006736927, + "noise_ratio": 0.07272119750519898, + "noisy": false + }, + { + "driver": "psycopg_sync", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "session_per_request", + "time": 0.17403190799814183, + "times": [ + 0.18479846199625172, + 0.15965048600628506, + 0.17403190799814183 + ], + "stddev": 0.012617214958764451, + "iqr": 0.012573987994983327, + "noise_ratio": 0.07249943475250048, + "noisy": false + }, + { + "driver": "psycopg_sync", + "library": "raw", + "library_key": "raw", + "scenario": "observability_select", + "time": 0.05511280300561339, + "times": [ + 0.05511280300561339, + 0.055044888009433635, + 0.055865646005258895 + ], + "stddev": 0.00045552693234181046, + "iqr": 0.00041037899791263044, + "noise_ratio": 0.008265355915492337, + "noisy": false + }, + { + "driver": "psycopg_sync", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "observability_select", + "time": 0.09858430500025861, + "times": [ + 0.10176454100292176, + 0.0939475999912247, + 0.09858430500025861 + ], + "stddev": 0.003931019824290434, + "iqr": 0.003908470505848527, + "noise_ratio": 0.03987470241110004, + "noisy": false + }, { "driver": "psycopg_async", "library": "raw", "library_key": "raw", "scenario": "rows", - "time": 0.20733329094946384, + "time": 0.10590479400707409, "times": [ - 0.20733329094946384, - 0.1942386640002951, - 0.2190431049093604 + 0.10598615701019298, + 0.10503190700546838, + 0.10590479400707409 ], - "stddev": 0.01240866153623211, - "iqr": 0.012402220454532653, - "noise_ratio": 0.059848862087741816, + "stddev": 0.0005290155522864491, + "iqr": 0.0004771250023622997, + "noise_ratio": 0.004995199294293633, "noisy": false }, { @@ -60,15 +232,175 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "rows", - "time": 0.214356982964091, + "time": 0.11431658400397282, "times": [ - 0.1982118720188737, - 0.214356982964091, - 0.2291014059446752 + 0.11752552099642344, + 0.11194133700337261, + 0.11431658400397282 ], - "stddev": 0.015450058913679468, - "iqr": 0.015444766962900758, - "noise_ratio": 0.07207630327707895, + "stddev": 0.002802444938609363, + "iqr": 0.0027920919965254143, + "noise_ratio": 0.024514771527042567, + "noisy": false + }, + { + "driver": "psycopg_async", + "library": "raw", + "library_key": "raw", + "scenario": "scalar_param_select", + "time": 0.0582181589998072, + "times": [ + 0.0582181589998072, + 0.060979214002145454, + 0.056073824991472065 + ], + "stddev": 0.0024591473593935957, + "iqr": 0.0024526945053366944, + "noise_ratio": 0.042240211673504474, + "noisy": false + }, + { + "driver": "psycopg_async", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "scalar_param_select", + "time": 0.08660450000024866, + "times": [ + 0.08050378099142108, + 0.09873811498982832, + 0.08660450000024866 + ], + "stddev": 0.009282010571579596, + "iqr": 0.009117166999203619, + "noise_ratio": 0.10717700086661716, + "noisy": true + }, + { + "driver": "psycopg_async", + "library": "raw", + "library_key": "raw", + "scenario": "no_param_select", + "time": 0.052887986006680876, + "times": [ + 0.05260565900243819, + 0.052887986006680876, + 0.05682529001205694 + ], + "stddev": 0.0023589318636977486, + "iqr": 0.002109815504809376, + "noise_ratio": 0.044602414306337275, + "noisy": false + }, + { + "driver": "psycopg_async", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "no_param_select", + "time": 0.08167915900412481, + "times": [ + 0.08225427399156615, + 0.08167915900412481, + 0.07951909299299587 + ], + "stddev": 0.0014420967094335316, + "iqr": 0.0013675904992851429, + "noise_ratio": 0.017655626319079822, + "noisy": false + }, + { + "driver": "psycopg_async", + "library": "raw", + "library_key": "raw", + "scenario": "complex_query", + "time": 0.08404070899996441, + "times": [ + 0.08615902000747155, + 0.0772992849961156, + 0.08404070899996441 + ], + "stddev": 0.004626534765092214, + "iqr": 0.004429867505677976, + "noise_ratio": 0.05505111534808891, + "noisy": false + }, + { + "driver": "psycopg_async", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "complex_query", + "time": 0.11186880600871518, + "times": [ + 0.10963978800282348, + 0.11186880600871518, + 0.12448533900897019 + ], + "stddev": 0.008005579417852031, + "iqr": 0.007422775503073353, + "noise_ratio": 0.07156221384206383, + "noisy": false + }, + { + "driver": "psycopg_async", + "library": "raw", + "library_key": "raw", + "scenario": "session_per_request", + "time": 0.08126786199864, + "times": [ + 0.09097665699664503, + 0.08126786199864, + 0.07808626499900129 + ], + "stddev": 0.00671497661492969, + "iqr": 0.006445195998821873, + "noise_ratio": 0.08262770115746447, + "noisy": false + }, + { + "driver": "psycopg_async", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "session_per_request", + "time": 0.23540373700961936, + "times": [ + 0.23760637899977155, + 0.23540373700961936, + 0.22950175899313763 + ], + "stddev": 0.00419066107942701, + "iqr": 0.004052310003316961, + "noise_ratio": 0.01780201594359467, + "noisy": false + }, + { + "driver": "psycopg_async", + "library": "raw", + "library_key": "raw", + "scenario": "observability_select", + "time": 0.0786619019927457, + "times": [ + 0.0786619019927457, + 0.07817396400787402, + 0.08226269800798036 + ], + "stddev": 0.0022331428206317987, + "iqr": 0.002044367000053171, + "noise_ratio": 0.02838912820640597, + "noisy": false + }, + { + "driver": "psycopg_async", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "observability_select", + "time": 0.12231443398923147, + "times": [ + 0.12231443398923147, + 0.13678004599933047, + 0.11825213000702206 + ], + "stddev": 0.009738582335985667, + "iqr": 0.009263957996154204, + "noise_ratio": 0.07961924049653084, "noisy": false }, { @@ -76,31 +408,31 @@ "library": "raw", "library_key": "raw", "scenario": "initialization", - "time": 0.044801491079851985, + "time": 0.016252656001597643, "times": [ - 0.03780574502889067, - 0.044801491079851985, - 0.05556856491602957 + 0.01644538099935744, + 0.016108208990772255, + 0.016252656001597643 ], - "stddev": 0.008947887200769595, - "iqr": 0.008881409943569452, - "noise_ratio": 0.19972297763084088, - "noisy": true + "stddev": 0.0001691610805668527, + "iqr": 0.0001685860042925924, + "noise_ratio": 0.010408211467111843, + "noisy": false }, { "driver": "asyncpg", "library": "sqlspec", "library_key": "sqlspec", "scenario": "initialization", - "time": 0.03986536001320928, + "time": 0.02152997899975162, "times": [ - 0.045165335992351174, - 0.03986536001320928, - 0.0387102939421311 + 0.022655359003692865, + 0.021521306000067852, + 0.02152997899975162 ], - "stddev": 0.0034421769003095118, - "iqr": 0.003227521025110036, - "noise_ratio": 0.08634505995101902, + "stddev": 0.0006522565432436874, + "iqr": 0.0005670265018125065, + "noise_ratio": 0.03029527075949364, "noisy": false }, { @@ -108,31 +440,31 @@ "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "initialization", - "time": 0.04134847305249423, + "time": 0.020192680996842682, "times": [ - 0.04227136599365622, - 0.03727546101436019, - 0.04134847305249423 + 0.019890349998604506, + 0.020192680996842682, + 0.025548206001985818 ], - "stddev": 0.002658327325148475, - "iqr": 0.0024979524896480143, - "noise_ratio": 0.0642908221005786, - "noisy": false + "stddev": 0.0031828809452079474, + "iqr": 0.002828928001690656, + "noise_ratio": 0.15762547557234333, + "noisy": true }, { "driver": "asyncpg", "library": "raw", "library_key": "raw", "scenario": "write_heavy", - "time": 0.0446074609644711, + "time": 0.024334375993930735, "times": [ - 0.0446074609644711, - 0.04706162097863853, - 0.040620596962980926 + 0.024462239001877606, + 0.024334375993930735, + 0.02381368400529027 ], - "stddev": 0.0032507634055145293, - "iqr": 0.003220512007828802, - "noise_ratio": 0.07287488091069998, + "stddev": 0.00034353376336588887, + "iqr": 0.00032427749829366803, + "noise_ratio": 0.014117220982020251, "noisy": false }, { @@ -140,31 +472,31 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "write_heavy", - "time": 0.05116820405237377, + "time": 0.030328352993819863, "times": [ - 0.05116820405237377, - 0.048695035045966506, - 0.05957270704675466 + 0.030328352993819863, + 0.02966713800560683, + 0.030997699999716133 ], - "stddev": 0.005701987718821413, - "iqr": 0.005438836000394076, - "noise_ratio": 0.11143615111026922, - "noisy": true + "stddev": 0.0006652851387573084, + "iqr": 0.0006652809970546514, + "noise_ratio": 0.021936078721217615, + "noisy": false }, { "driver": "asyncpg", "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "write_heavy", - "time": 0.04844018898438662, + "time": 0.02888492999773007, "times": [ - 0.04844018898438662, - 0.04814719106070697, - 0.05589364701882005 + 0.02888492999773007, + 0.02834993999567814, + 0.030016777993296273 ], - "stddev": 0.0043902821542836, - "iqr": 0.003873227979056537, - "noise_ratio": 0.09063305173517569, + "stddev": 0.0008510428012803251, + "iqr": 0.0008334189988090657, + "noise_ratio": 0.029463211485961868, "noisy": false }, { @@ -172,15 +504,15 @@ "library": "raw", "library_key": "raw", "scenario": "read_heavy", - "time": 0.03530651098117232, + "time": 0.0118706149951322, "times": [ - 0.036079684970900416, - 0.03530651098117232, - 0.0308308299863711 + 0.0118706149951322, + 0.012281999006518163, + 0.011099155992269516 ], - "stddev": 0.0028337253758446428, - "iqr": 0.002624427492264658, - "noise_ratio": 0.0802607025473507, + "stddev": 0.0006004863853964159, + "iqr": 0.0005914215071243234, + "noise_ratio": 0.050585954109593996, "noisy": false }, { @@ -188,15 +520,15 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "read_heavy", - "time": 0.03375874599441886, + "time": 0.017022311993059702, "times": [ - 0.03375874599441886, - 0.03446531796362251, - 0.03358696599025279 + 0.018128079012967646, + 0.015996726011508144, + 0.017022311993059702 ], - "stddev": 0.0004655202298894747, - "iqr": 0.0004391759866848588, - "noise_ratio": 0.013789618547040713, + "stddev": 0.0010659278371995603, + "iqr": 0.0010656765007297508, + "noise_ratio": 0.062619451319783, "noisy": false }, { @@ -204,15 +536,15 @@ "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "read_heavy", - "time": 0.03504969098139554, + "time": 0.015185387004748918, "times": [ - 0.03504969098139554, - 0.03519547206815332, - 0.03410959802567959 + 0.015380247990833595, + 0.015185387004748918, + 0.014229471009457484 ], - "stddev": 0.0005893710385884947, - "iqr": 0.0005429370212368667, - "noise_ratio": 0.01681529913919453, + "stddev": 0.0006159050055023395, + "iqr": 0.0005753884906880558, + "noise_ratio": 0.040559058870855765, "noisy": false }, { @@ -220,31 +552,31 @@ "library": "raw", "library_key": "raw", "scenario": "iterative_inserts", - "time": 0.3479909528978169, + "time": 0.6901849880086957, "times": [ - 0.3479909528978169, - 0.337999137933366, - 0.40759251394774765 + 0.6400381919956999, + 0.7464155230118195, + 0.6901849880086957 ], - "stddev": 0.03762849193308965, - "iqr": 0.034796688007190824, - "noise_ratio": 0.1081306615006706, - "noisy": true + "stddev": 0.0532176517883863, + "iqr": 0.053188665508059785, + "noise_ratio": 0.0771063594731733, + "noisy": false }, { "driver": "asyncpg", "library": "sqlspec", "library_key": "sqlspec", "scenario": "iterative_inserts", - "time": 0.4137253829976544, + "time": 0.7482371899968712, "times": [ - 0.4137253829976544, - 0.41692268394399434, - 0.41216089599765837 + 0.7289663100091275, + 0.7482371899968712, + 0.7537915709981462 ], - "stddev": 0.002427103121023548, - "iqr": 0.0023808939731679857, - "noise_ratio": 0.005866459300703114, + "stddev": 0.013028888076004027, + "iqr": 0.012412630494509358, + "noise_ratio": 0.017412777993644647, "noisy": false }, { @@ -252,15 +584,15 @@ "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "iterative_inserts", - "time": 0.343022566055879, + "time": 0.24287494999589399, "times": [ - 0.3223919089650735, - 0.35162515798583627, - 0.343022566055879 + 0.24287494999589399, + 0.24870659499720205, + 0.21582980499078985 ], - "stddev": 0.01502337871575329, - "iqr": 0.01461662451038137, - "noise_ratio": 0.043797056527487915, + "stddev": 0.017542003272593636, + "iqr": 0.016438395003206097, + "noise_ratio": 0.07222648228189114, "noisy": false }, { @@ -268,15 +600,15 @@ "library": "raw", "library_key": "raw", "scenario": "repeated_queries", - "time": 0.21294607897289097, + "time": 0.10828063200460747, "times": [ - 0.21294607897289097, - 0.23929880699142814, - 0.21020107390359044 + 0.10828063200460747, + 0.10793121599999722, + 0.12519359499856364 ], - "stddev": 0.016065902805323058, - "iqr": 0.014548866543918848, - "noise_ratio": 0.07544587288394412, + "stddev": 0.009867118267840883, + "iqr": 0.00863118949928321, + "noise_ratio": 0.09112542183371283, "noisy": false }, { @@ -284,15 +616,15 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "repeated_queries", - "time": 0.4103268129983917, + "time": 0.22818390501197428, "times": [ - 0.3971549429697916, - 0.4103268129983917, - 0.4249703430105001 + 0.22818390501197428, + 0.2235217840061523, + 0.24140914900635835 ], - "stddev": 0.013914187068663035, - "iqr": 0.013907700020354241, - "noise_ratio": 0.033910011795201825, + "stddev": 0.009279011061121714, + "iqr": 0.008943682500103023, + "noise_ratio": 0.04066461681701338, "noisy": false }, { @@ -300,15 +632,15 @@ "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "repeated_queries", - "time": 0.5083378970157355, + "time": 0.36793276100070216, "times": [ - 0.5083378970157355, - 0.5062282889848575, - 0.5189653070410714 + 0.36793276100070216, + 0.3864833539992105, + 0.34751537900592666 ], - "stddev": 0.006826711248896642, - "iqr": 0.006368509028106928, - "noise_ratio": 0.013429475333186348, + "stddev": 0.019491438561558622, + "iqr": 0.01948398749664193, + "noise_ratio": 0.05297554506575027, "noisy": false }, { @@ -316,32 +648,384 @@ "library": "raw", "library_key": "raw", "scenario": "rows", - "time": 0.04551889596041292, + "time": 0.029271915002027526, + "times": [ + 0.029271915002027526, + 0.027956382997217588, + 0.03000156099733431 + ], + "stddev": 0.0010364812899346769, + "iqr": 0.0010225890000583604, + "noise_ratio": 0.03540872846422534, + "noisy": false + }, + { + "driver": "asyncpg", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "rows", + "time": 0.03579180400993209, + "times": [ + 0.03753435499675106, + 0.03579180400993209, + 0.03416556199954357 + ], + "stddev": 0.0016847311000102515, + "iqr": 0.0016843964986037463, + "noise_ratio": 0.04707030412724502, + "noisy": false + }, + { + "driver": "asyncpg", + "library": "raw", + "library_key": "raw", + "scenario": "scalar_param_select", + "time": 0.04725500299537089, + "times": [ + 0.05113000300480053, + 0.0470034790050704, + 0.04725500299537089 + ], + "stddev": 0.0023132621162593853, + "iqr": 0.0020632619998650625, + "noise_ratio": 0.04895274509846065, + "noisy": false + }, + { + "driver": "asyncpg", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "scalar_param_select", + "time": 0.08483455299574416, + "times": [ + 0.08483455299574416, + 0.07786131300963461, + 0.09271926200017333 + ], + "stddev": 0.007433632582098898, + "iqr": 0.007428974495269358, + "noise_ratio": 0.08762505747477464, + "noisy": false + }, + { + "driver": "asyncpg", + "library": "raw", + "library_key": "raw", + "scenario": "no_param_select", + "time": 0.04900928599818144, + "times": [ + 0.052870790008455515, + 0.04900928599818144, + 0.047410389001015574 + ], + "stddev": 0.002807242537644367, + "iqr": 0.0027302005037199706, + "noise_ratio": 0.05727980892740477, + "noisy": false + }, + { + "driver": "asyncpg", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "no_param_select", + "time": 0.06851341199944727, + "times": [ + 0.06921851700462867, + 0.068030488997465, + 0.06851341199944727 + ], + "stddev": 0.0005974666294698942, + "iqr": 0.0005940140035818331, + "noise_ratio": 0.008720433153653393, + "noisy": false + }, + { + "driver": "asyncpg", + "library": "raw", + "library_key": "raw", + "scenario": "complex_query", + "time": 0.08479894499760121, + "times": [ + 0.0980167999950936, + 0.08479894499760121, + 0.07594491800409742 + ], + "stddev": 0.011107605947884618, + "iqr": 0.011035940995498095, + "noise_ratio": 0.13098754882149571, + "noisy": true + }, + { + "driver": "asyncpg", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "complex_query", + "time": 0.11133583699120209, + "times": [ + 0.10561610999866389, + 0.11678107899206225, + 0.11133583699120209 + ], + "stddev": 0.005583046807682589, + "iqr": 0.0055824844966991805, + "noise_ratio": 0.05014599933464163, + "noisy": false + }, + { + "driver": "asyncpg", + "library": "raw", + "library_key": "raw", + "scenario": "session_per_request", + "time": 0.0681627669982845, "times": [ - 0.04551889596041292, - 0.04794929292984307, - 0.044716733042150736 + 0.082068016010453, + 0.06561306599178351, + 0.0681627669982845 ], - "stddev": 0.0016832378621755362, - "iqr": 0.0016162799438461661, - "noise_ratio": 0.03697888155370513, + "stddev": 0.0088564694379295, + "iqr": 0.00822747500933474, + "noise_ratio": 0.12993119011956186, + "noisy": true + }, + { + "driver": "asyncpg", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "session_per_request", + "time": 0.19334729299589526, + "times": [ + 0.19144087300810497, + 0.19334729299589526, + 0.20283524499973282 + ], + "stddev": 0.006103105541178261, + "iqr": 0.005697185995813925, + "noise_ratio": 0.03156550808967276, + "noisy": false + }, + { + "driver": "asyncpg", + "library": "raw", + "library_key": "raw", + "scenario": "observability_select", + "time": 0.06520503900537733, + "times": [ + 0.06520503900537733, + 0.06378934299573302, + 0.06650181100121699 + ], + "stddev": 0.0013566684366301508, + "iqr": 0.001356234002741985, + "noise_ratio": 0.020806190093962968, "noisy": false }, { "driver": "asyncpg", "library": "sqlspec", "library_key": "sqlspec", + "scenario": "observability_select", + "time": 0.1334093390032649, + "times": [ + 0.11765947099775076, + 0.1334093390032649, + 0.144385246996535 + ], + "stddev": 0.01343376344145005, + "iqr": 0.013362887999392115, + "noise_ratio": 0.10069582490863917, + "noisy": true + }, + { + "driver": "psqlpy", + "library": "raw", + "library_key": "raw", + "scenario": "rows", + "time": 0.03865024499827996, + "times": [ + 0.03877540898974985, + 0.03820392600027844, + 0.03865024499827996 + ], + "stddev": 0.00030040513754842437, + "iqr": 0.00028574149473570287, + "noise_ratio": 0.007772399309804976, + "noisy": false + }, + { + "driver": "psqlpy", + "library": "sqlspec", + "library_key": "sqlspec", "scenario": "rows", - "time": 0.05420948297251016, + "time": 0.043907539002248086, + "times": [ + 0.04820335599652026, + 0.043810168994241394, + 0.043907539002248086 + ], + "stddev": 0.002508771831120053, + "iqr": 0.002196593501139432, + "noise_ratio": 0.05713760980754587, + "noisy": false + }, + { + "driver": "psqlpy", + "library": "raw", + "library_key": "raw", + "scenario": "scalar_param_select", + "time": 0.1293873389950022, + "times": [ + 0.120815243004472, + 0.1293873389950022, + 0.13075926499732304 + ], + "stddev": 0.00538897924813555, + "iqr": 0.004972010996425524, + "noise_ratio": 0.04164997355996097, + "noisy": false + }, + { + "driver": "psqlpy", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "scalar_param_select", + "time": 0.18837299499136861, + "times": [ + 0.19009797599574085, + 0.18837299499136861, + 0.18020312799490057 + ], + "stddev": 0.005285677237673456, + "iqr": 0.004947424000420142, + "noise_ratio": 0.02805963369598519, + "noisy": false + }, + { + "driver": "psqlpy", + "library": "raw", + "library_key": "raw", + "scenario": "no_param_select", + "time": 0.17247518899966963, + "times": [ + 0.1658194710034877, + 0.17247518899966963, + 0.18021090600814205 + ], + "stddev": 0.007202468340472587, + "iqr": 0.00719571750232717, + "noise_ratio": 0.041759445994788175, + "noisy": false + }, + { + "driver": "psqlpy", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "no_param_select", + "time": 0.18234803499944974, + "times": [ + 0.18234803499944974, + 0.18064538700855337, + 0.22009064500161912 + ], + "stddev": 0.022298475277083698, + "iqr": 0.019722628996532876, + "noise_ratio": 0.12228525126224139, + "noisy": true + }, + { + "driver": "psqlpy", + "library": "raw", + "library_key": "raw", + "scenario": "complex_query", + "time": 0.18290463100129273, + "times": [ + 0.18290463100129273, + 0.1759164170071017, + 0.1966316300095059 + ], + "stddev": 0.010538703955223563, + "iqr": 0.010357606501202099, + "noise_ratio": 0.05761857366612592, + "noisy": false + }, + { + "driver": "psqlpy", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "complex_query", + "time": 0.23078126600012183, + "times": [ + 0.22463746600260492, + 0.23078126600012183, + 0.2807858739979565 + ], + "stddev": 0.030797323530460302, + "iqr": 0.028074203997675795, + "noise_ratio": 0.13344810895718046, + "noisy": true + }, + { + "driver": "psqlpy", + "library": "raw", + "library_key": "raw", + "scenario": "session_per_request", + "time": 0.17003524499887135, + "times": [ + 0.1603259289986454, + 0.17003524499887135, + 0.18832512600056361 + ], + "stddev": 0.014217041399910025, + "iqr": 0.01399959850095911, + "noise_ratio": 0.08361232049275663, + "noisy": false + }, + { + "driver": "psqlpy", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "session_per_request", + "time": 0.3086909220000962, + "times": [ + 0.34571038599824533, + 0.3086909220000962, + 0.2691083060053643 + ], + "stddev": 0.03830818638126933, + "iqr": 0.03830103999644052, + "noise_ratio": 0.1240988433772516, + "noisy": true + }, + { + "driver": "psqlpy", + "library": "raw", + "library_key": "raw", + "scenario": "observability_select", + "time": 0.1629070619965205, + "times": [ + 0.1510089889925439, + 0.17306879900570493, + 0.1629070619965205 + ], + "stddev": 0.011041288104719679, + "iqr": 0.01102990500658052, + "noise_ratio": 0.0677766081433321, + "noisy": false + }, + { + "driver": "psqlpy", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "observability_select", + "time": 0.227817266000784, "times": [ - 0.05420948297251016, - 0.05423876002896577, - 0.05306137597654015 + 0.227817266000784, + 0.228459699996165, + 0.22098791699681897 ], - "stddev": 0.0006714710253287391, - "iqr": 0.0005886920262128115, - "noise_ratio": 0.012386597113815763, + "stddev": 0.004140858816833689, + "iqr": 0.0037358914996730164, + "noise_ratio": 0.01817622908712915, "noisy": false } ] -} +} \ No newline at end of file diff --git a/tools/perf_baselines/psqlpy.json b/tools/perf_baselines/psqlpy.json new file mode 100644 index 000000000..17727b0bc --- /dev/null +++ b/tools/perf_baselines/psqlpy.json @@ -0,0 +1,215 @@ +{ + "metadata": { + "rows": 1000, + "pool_size": 1, + "iterations": 3, + "warmup": 1, + "mypyc_compiled": false, + "environment": { + "python_version": "3.11.9", + "platform": "Linux-7.1.6-1rodete1-amd64-x86_64-with-glibc2.43", + "packages": { + "duckdb": "1.5.6", + "sqlglot": "30.22.0", + "aiosqlite": "0.22.1", + "pyarrow": "26.0.0", + "sqlspec": "0.66.0" + } + }, + "timestamp": "2026-10-13T21:42:28+0000" + }, + "results": [ + { + "driver": "psqlpy", + "library": "raw", + "library_key": "raw", + "scenario": "rows", + "time": 0.038870181000675075, + "times": [ + 0.03749169000366237, + 0.03915389100438915, + 0.038870181000675075 + ], + "stddev": 0.0008891607430483382, + "iqr": 0.0008311005003633909, + "noise_ratio": 0.02287513770601932, + "noisy": false + }, + { + "driver": "psqlpy", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "rows", + "time": 0.04553226499410812, + "times": [ + 0.04509593699185643, + 0.04553226499410812, + 0.057247064003604464 + ], + "stddev": 0.006892952749174069, + "iqr": 0.006075563505874015, + "noise_ratio": 0.15138611597876842, + "noisy": true + }, + { + "driver": "psqlpy", + "library": "raw", + "library_key": "raw", + "scenario": "scalar_param_select", + "time": 0.13926593400537968, + "times": [ + 0.1660893600055715, + 0.13099403699743561, + 0.13926593400537968 + ], + "stddev": 0.01834667163067638, + "iqr": 0.017547661504067946, + "noise_ratio": 0.13173840222812616, + "noisy": true + }, + { + "driver": "psqlpy", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "scalar_param_select", + "time": 0.1755439479893539, + "times": [ + 0.1893631630082382, + 0.17003162500623148, + 0.1755439479893539 + ], + "stddev": 0.009958788161982412, + "iqr": 0.009665769001003355, + "noise_ratio": 0.0567310253417929, + "noisy": false + }, + { + "driver": "psqlpy", + "library": "raw", + "library_key": "raw", + "scenario": "no_param_select", + "time": 0.16983815999992657, + "times": [ + 0.1830866660020547, + 0.16983815999992657, + 0.15680248399439733 + ], + "stddev": 0.013142234614790685, + "iqr": 0.013142091003828682, + "noise_ratio": 0.07738092908446703, + "noisy": false + }, + { + "driver": "psqlpy", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "no_param_select", + "time": 0.16322541900444776, + "times": [ + 0.14927623599942308, + 0.16717540299578104, + 0.16322541900444776 + ], + "stddev": 0.009403565451492044, + "iqr": 0.008949583498178981, + "noise_ratio": 0.05761091323181596, + "noisy": false + }, + { + "driver": "psqlpy", + "library": "raw", + "library_key": "raw", + "scenario": "complex_query", + "time": 0.15418924599362072, + "times": [ + 0.1493776040006196, + 0.16773134699906223, + 0.15418924599362072 + ], + "stddev": 0.009516654527399783, + "iqr": 0.009176871499221306, + "noise_ratio": 0.061720611356991244, + "noisy": false + }, + { + "driver": "psqlpy", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "complex_query", + "time": 0.21580600099696312, + "times": [ + 0.22752288200717885, + 0.21562466000614222, + 0.21580600099696312 + ], + "stddev": 0.006817695993374619, + "iqr": 0.0059491110005183145, + "noise_ratio": 0.031591781330819246, + "noisy": false + }, + { + "driver": "psqlpy", + "library": "raw", + "library_key": "raw", + "scenario": "session_per_request", + "time": 0.14990542401210405, + "times": [ + 0.15869152099185158, + 0.14990542401210405, + 0.1469935929926578 + ], + "stddev": 0.006089824566406652, + "iqr": 0.00584896399959689, + "noise_ratio": 0.04062444442247088, + "noisy": false + }, + { + "driver": "psqlpy", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "session_per_request", + "time": 0.2788829950004583, + "times": [ + 0.29392264400667045, + 0.2788829950004583, + 0.26725216799241025 + ], + "stddev": 0.013371496238569232, + "iqr": 0.013335238007130101, + "noise_ratio": 0.047946617320813185, + "noisy": false + }, + { + "driver": "psqlpy", + "library": "raw", + "library_key": "raw", + "scenario": "observability_select", + "time": 0.15503014900605194, + "times": [ + 0.15503014900605194, + 0.15803829400101677, + 0.14209970699448604 + ], + "stddev": 0.008468410148801548, + "iqr": 0.007969293503265362, + "noise_ratio": 0.05462427923275082, + "noisy": false + }, + { + "driver": "psqlpy", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "observability_select", + "time": 0.21762880399182905, + "times": [ + 0.21762880399182905, + 0.23060299998905975, + 0.21332993000396527 + ], + "stddev": 0.00899230250499523, + "iqr": 0.00863653499254724, + "noise_ratio": 0.041319450091417353, + "noisy": false + } + ] +} \ No newline at end of file diff --git a/tools/perf_baselines/psycopg_async.json b/tools/perf_baselines/psycopg_async.json index 3a4a19ad4..038669615 100644 --- a/tools/perf_baselines/psycopg_async.json +++ b/tools/perf_baselines/psycopg_async.json @@ -3,8 +3,20 @@ "rows": 1000, "pool_size": 1, "iterations": 3, + "warmup": 1, "mypyc_compiled": false, - "timestamp": "2026-07-09T20:52:38+0000" + "environment": { + "python_version": "3.11.9", + "platform": "Linux-7.1.6-1rodete1-amd64-x86_64-with-glibc2.43", + "packages": { + "duckdb": "1.5.6", + "sqlglot": "30.22.0", + "aiosqlite": "0.22.1", + "pyarrow": "26.0.0", + "sqlspec": "0.66.0" + } + }, + "timestamp": "2026-10-13T21:40:20+0000" }, "results": [ { @@ -12,15 +24,15 @@ "library": "raw", "library_key": "raw", "scenario": "rows", - "time": 0.2959300350630656, + "time": 0.10732070899393875, "times": [ - 0.28633909800555557, - 0.29886266810353845, - 0.2959300350630656 + 0.10732070899393875, + 0.10796528100036085, + 0.10532009199960157 ], - "stddev": 0.006550142723512253, - "iqr": 0.006261785048991442, - "noise_ratio": 0.02213409234421357, + "stddev": 0.0013793093851170338, + "iqr": 0.0013225945003796369, + "noise_ratio": 0.01285222021031313, "noisy": false }, { @@ -28,16 +40,176 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "rows", - "time": 0.34546393796335906, + "time": 0.114254736006842, + "times": [ + 0.11448235799616668, + 0.114254736006842, + 0.11244962899945676 + ], + "stddev": 0.0011137182657058229, + "iqr": 0.0010163644983549602, + "noise_ratio": 0.009747677029678046, + "noisy": false + }, + { + "driver": "psycopg_async", + "library": "raw", + "library_key": "raw", + "scenario": "scalar_param_select", + "time": 0.06272070900013205, + "times": [ + 0.054481760002090596, + 0.0634723289986141, + 0.06272070900013205 + ], + "stddev": 0.004987911120828305, + "iqr": 0.0044952844982617535, + "noise_ratio": 0.07952574517003314, + "noisy": false + }, + { + "driver": "psycopg_async", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "scalar_param_select", + "time": 0.08722766900609713, + "times": [ + 0.08722766900609713, + 0.11313651800446678, + 0.08694001298863441 + ], + "stddev": 0.015042207713903086, + "iqr": 0.013098252507916186, + "noise_ratio": 0.17244766351433338, + "noisy": true + }, + { + "driver": "psycopg_async", + "library": "raw", + "library_key": "raw", + "scenario": "no_param_select", + "time": 0.05285361300047953, + "times": [ + 0.05285361300047953, + 0.051201517999288626, + 0.05827487500209827 + ], + "stddev": 0.0037002675324979647, + "iqr": 0.003536678501404822, + "noise_ratio": 0.07000973675090846, + "noisy": false + }, + { + "driver": "psycopg_async", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "no_param_select", + "time": 0.08752505099982955, + "times": [ + 0.08850791800068691, + 0.08752505099982955, + 0.07754960999591276 + ], + "stddev": 0.0060630020766874225, + "iqr": 0.005479154002387077, + "noise_ratio": 0.06927162003823603, + "noisy": false + }, + { + "driver": "psycopg_async", + "library": "raw", + "library_key": "raw", + "scenario": "complex_query", + "time": 0.07898365501023363, "times": [ - 0.30046050203964114, - 0.3557198899798095, - 0.34546393796335906 + 0.07922200000029989, + 0.07639972800097894, + 0.07898365501023363 ], - "stddev": 0.029394142705154558, - "iqr": 0.02762969397008419, - "noise_ratio": 0.08508599444111063, + "stddev": 0.0015651787097962458, + "iqr": 0.0014111359996604733, + "noise_ratio": 0.019816488735466233, "noisy": false + }, + { + "driver": "psycopg_async", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "complex_query", + "time": 0.12235170499479864, + "times": [ + 0.10542201099451631, + 0.12449819299217779, + 0.12235170499479864 + ], + "stddev": 0.010449263684378212, + "iqr": 0.00953809099883074, + "noise_ratio": 0.08540349874832089, + "noisy": false + }, + { + "driver": "psycopg_async", + "library": "raw", + "library_key": "raw", + "scenario": "session_per_request", + "time": 0.08765066500927787, + "times": [ + 0.09017416898859665, + 0.08765066500927787, + 0.08010931899480056 + ], + "stddev": 0.005236747995994709, + "iqr": 0.005032424996898044, + "noise_ratio": 0.05974567329797665, + "noisy": false + }, + { + "driver": "psycopg_async", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "session_per_request", + "time": 0.23639956300030462, + "times": [ + 0.25069692500983365, + 0.23639956300030462, + 0.2202673319989117 + ], + "stddev": 0.015224013751987231, + "iqr": 0.015214796505460981, + "noise_ratio": 0.06439950039995469, + "noisy": false + }, + { + "driver": "psycopg_async", + "library": "raw", + "library_key": "raw", + "scenario": "observability_select", + "time": 0.0797479739994742, + "times": [ + 0.07919690600829199, + 0.0797479739994742, + 0.08318662300007418 + ], + "stddev": 0.0021620138973785664, + "iqr": 0.001994858495891094, + "noise_ratio": 0.027110580858051906, + "noisy": false + }, + { + "driver": "psycopg_async", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "observability_select", + "time": 0.12852915600524284, + "times": [ + 0.12852915600524284, + 0.12195321000763215, + 0.15071062499191612 + ], + "stddev": 0.015067899371116515, + "iqr": 0.014378707492141984, + "noise_ratio": 0.11723331763340825, + "noisy": true } ] -} +} \ No newline at end of file diff --git a/tools/perf_baselines/psycopg_sync.json b/tools/perf_baselines/psycopg_sync.json index e4c89de62..1f80c8bab 100644 --- a/tools/perf_baselines/psycopg_sync.json +++ b/tools/perf_baselines/psycopg_sync.json @@ -3,8 +3,20 @@ "rows": 1000, "pool_size": 1, "iterations": 3, + "warmup": 1, "mypyc_compiled": false, - "timestamp": "2026-07-09T20:52:32+0000" + "environment": { + "python_version": "3.11.9", + "platform": "Linux-7.1.6-1rodete1-amd64-x86_64-with-glibc2.43", + "packages": { + "duckdb": "1.5.6", + "sqlglot": "30.22.0", + "aiosqlite": "0.22.1", + "pyarrow": "26.0.0", + "sqlspec": "0.66.0" + } + }, + "timestamp": "2026-10-13T21:40:14+0000" }, "results": [ { @@ -12,32 +24,192 @@ "library": "raw", "library_key": "raw", "scenario": "rows", - "time": 0.050552292028442025, + "time": 0.030215855993446894, "times": [ - 0.050552292028442025, - 0.04806464398279786, - 0.05370596796274185 + 0.030215855993446894, + 0.041531951996148564, + 0.028833802993176505 ], - "stddev": 0.0028272071302574155, - "iqr": 0.0028206619899719954, - "noise_ratio": 0.055926388632720266, - "noisy": false + "stddev": 0.006966671698637705, + "iqr": 0.0063490745014860295, + "noise_ratio": 0.23056343994188389, + "noisy": true }, { "driver": "psycopg_sync", "library": "sqlspec", "library_key": "sqlspec", "scenario": "rows", - "time": 0.06116347201168537, + "time": 0.045102126998244785, + "times": [ + 0.045102126998244785, + 0.04464112599089276, + 0.045206759008578956 + ], + "stddev": 0.0003009458691810825, + "iqr": 0.00028281650884309784, + "noise_ratio": 0.0066725427204972895, + "noisy": false + }, + { + "driver": "psycopg_sync", + "library": "raw", + "library_key": "raw", + "scenario": "scalar_param_select", + "time": 0.047244686997146346, + "times": [ + 0.047244686997146346, + 0.05311737199372146, + 0.042004770002677105 + ], + "stddev": 0.005559302746887255, + "iqr": 0.005556300995522179, + "noise_ratio": 0.1176704323858267, + "noisy": true + }, + { + "driver": "psycopg_sync", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "scalar_param_select", + "time": 0.06822669900429901, + "times": [ + 0.06822669900429901, + 0.06704405600612517, + 0.06837901299877558 + ], + "stddev": 0.0007307478117505397, + "iqr": 0.0006674784963252023, + "noise_ratio": 0.010710584308123933, + "noisy": false + }, + { + "driver": "psycopg_sync", + "library": "raw", + "library_key": "raw", + "scenario": "no_param_select", + "time": 0.04043869000452105, + "times": [ + 0.03942789099528454, + 0.04106943399528973, + 0.04043869000452105 + ], + "stddev": 0.0008280716595229272, + "iqr": 0.0008207715000025928, + "noise_ratio": 0.020477212773963464, + "noisy": false + }, + { + "driver": "psycopg_sync", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "no_param_select", + "time": 0.06454010300512891, + "times": [ + 0.08719137999287341, + 0.06454010300512891, + 0.0621460179972928 + ], + "stddev": 0.013820770297454132, + "iqr": 0.012522680997790303, + "noise_ratio": 0.21414236504016448, + "noisy": true + }, + { + "driver": "psycopg_sync", + "library": "raw", + "library_key": "raw", + "scenario": "complex_query", + "time": 0.06197537999833003, + "times": [ + 0.06197537999833003, + 0.06380819100013468, + 0.06135116799850948 + ], + "stddev": 0.001277092976227095, + "iqr": 0.0012285115008126013, + "noise_ratio": 0.020606456568100222, + "noisy": false + }, + { + "driver": "psycopg_sync", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "complex_query", + "time": 0.1180369280045852, + "times": [ + 0.09941287100082263, + 0.1180369280045852, + 0.12877066800137982 + ], + "stddev": 0.014854566740703628, + "iqr": 0.014678898500278592, + "noise_ratio": 0.1258467751729916, + "noisy": true + }, + { + "driver": "psycopg_sync", + "library": "raw", + "library_key": "raw", + "scenario": "session_per_request", + "time": 0.057693398994160816, + "times": [ + 0.06089284199697431, + 0.057693398994160816, + 0.05657934599730652 + ], + "stddev": 0.002239188742498882, + "iqr": 0.0021567479998338968, + "noise_ratio": 0.03881187070856254, + "noisy": false + }, + { + "driver": "psycopg_sync", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "session_per_request", + "time": 0.18861053900036495, + "times": [ + 0.18909936299314722, + 0.1824087370041525, + 0.18861053900036495 + ], + "stddev": 0.003729740228236546, + "iqr": 0.0033453129944973625, + "noise_ratio": 0.019774824079312605, + "noisy": false + }, + { + "driver": "psycopg_sync", + "library": "raw", + "library_key": "raw", + "scenario": "observability_select", + "time": 0.05645457700302359, + "times": [ + 0.05598392299725674, + 0.05870470700028818, + 0.05645457700302359 + ], + "stddev": 0.001454147275260552, + "iqr": 0.00136039200151572, + "noise_ratio": 0.025757827840649148, + "noisy": false + }, + { + "driver": "psycopg_sync", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "observability_select", + "time": 0.11064468500262592, "times": [ - 0.06116347201168537, - 0.057821188936941326, - 0.07057346904184669 + 0.11064468500262592, + 0.09732882400567178, + 0.12210955600312445 ], - "stddev": 0.006612356465150814, - "iqr": 0.0063761400524526834, - "noise_ratio": 0.10810956683243086, + "stddev": 0.012401882246107186, + "iqr": 0.012390365998726338, + "noise_ratio": 0.11208746489551535, "noisy": true } ] -} +} \ No newline at end of file diff --git a/tools/perf_baselines/sqlite.json b/tools/perf_baselines/sqlite.json index a0527490d..0210af924 100644 --- a/tools/perf_baselines/sqlite.json +++ b/tools/perf_baselines/sqlite.json @@ -2,9 +2,21 @@ "metadata": { "rows": 1000, "pool_size": 1, - "iterations": 3, + "iterations": 5, + "warmup": 2, "mypyc_compiled": false, - "timestamp": "2026-07-09T20:51:14+0000" + "environment": { + "python_version": "3.11.9", + "platform": "Linux-7.1.6-1rodete1-amd64-x86_64-with-glibc2.43", + "packages": { + "duckdb": "1.5.6", + "sqlglot": "30.22.0", + "aiosqlite": "0.22.1", + "pyarrow": "26.0.0", + "sqlspec": "0.66.0" + } + }, + "timestamp": "2026-10-13T21:37:48+0000" }, "results": [ { @@ -12,15 +24,17 @@ "library": "raw", "library_key": "raw", "scenario": "initialization", - "time": 0.0030459959525614977, + "time": 0.002675202995305881, "times": [ - 0.002834903891198337, - 0.0030459959525614977, - 0.003190841991454363 + 0.0027365599962649867, + 0.002725298996665515, + 0.002611039992189035, + 0.002675202995305881, + 0.002634947988553904 ], - "stddev": 0.00017899355880746586, - "iqr": 0.0001779690501280129, - "noise_ratio": 0.058763557665578364, + "stddev": 5.477624766433942e-05, + "iqr": 9.035100811161101e-05, + "noise_ratio": 0.020475548121190835, "noisy": false }, { @@ -28,31 +42,35 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "initialization", - "time": 0.0009073169203475118, + "time": 0.00282091400003992, "times": [ - 0.0009711229940876365, - 0.0008872960461303592, - 0.0009073169203475118 + 0.0026993060018867254, + 0.0034525980008766055, + 0.0028972849977435544, + 0.0028015130083076656, + 0.00282091400003992 ], - "stddev": 4.377786285019206e-05, - "iqr": 4.191347397863865e-05, - "noise_ratio": 0.048249803203741295, - "noisy": false + "stddev": 0.0002982087669368678, + "iqr": 9.577198943588883e-05, + "noise_ratio": 0.10571352651397657, + "noisy": true }, { "driver": "sqlite", "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "initialization", - "time": 0.0018678000196814537, + "time": 0.0021299579966580495, "times": [ - 0.0018678000196814537, - 0.0016863120254129171, - 0.00200138206128031 + 0.002090784997562878, + 0.002119087992468849, + 0.002180947005399503, + 0.0021560580062214285, + 0.0021299579966580495 ], - "stddev": 0.00015814085567557345, - "iqr": 0.0001575350179336965, - "noise_ratio": 0.08466690973830474, + "stddev": 3.459578361278528e-05, + "iqr": 3.697001375257969e-05, + "noise_ratio": 0.016242472230469717, "noisy": false }, { @@ -60,15 +78,17 @@ "library": "raw", "library_key": "raw", "scenario": "write_heavy", - "time": 0.004611451993696392, + "time": 0.003833413007669151, "times": [ - 0.004869634984061122, - 0.004461181000806391, - 0.004611451993696392 + 0.0040214440086856484, + 0.003827246997389011, + 0.0036687200044980273, + 0.003833413007669151, + 0.0038556829968001693 ], - "stddev": 0.00020658915922448464, - "iqr": 0.00020422699162736535, - "noise_ratio": 0.044799156427711054, + "stddev": 0.0001252021174245056, + "iqr": 2.8435999411158264e-05, + "noise_ratio": 0.032660743096041424, "noisy": false }, { @@ -76,47 +96,53 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "write_heavy", - "time": 0.003128518001176417, + "time": 0.004287199990358204, "times": [ - 0.0025292159989476204, - 0.003128518001176417, - 0.0033896210370585322 + 0.004193825006950647, + 0.004254860003129579, + 0.004306560003897175, + 0.004420480006956495, + 0.004287199990358204 ], - "stddev": 0.0004411414200361265, - "iqr": 0.0004302025190554559, - "noise_ratio": 0.14100651486430446, - "noisy": true + "stddev": 8.330387668522975e-05, + "iqr": 5.1700000767596066e-05, + "noise_ratio": 0.01943083524738242, + "noisy": false }, { "driver": "sqlite", "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "write_heavy", - "time": 0.0061220829375088215, + "time": 0.005627367994748056, "times": [ - 0.0059563800459727645, - 0.0061220829375088215, - 0.008382795960642397 + 0.005526088003534824, + 0.005484861001605168, + 0.005846761996508576, + 0.005900725998799317, + 0.005627367994748056 ], - "stddev": 0.0013555918149275388, - "iqr": 0.0012132079573348165, - "noise_ratio": 0.22142656817373205, - "noisy": true + "stddev": 0.00018776813475699234, + "iqr": 0.0003206739929737523, + "noise_ratio": 0.033366955019155264, + "noisy": false }, { "driver": "sqlite", "library": "raw", "library_key": "raw", "scenario": "read_heavy", - "time": 0.0055305579444393516, + "time": 0.004371547998744063, "times": [ - 0.005601569078862667, - 0.005033050081692636, - 0.0055305579444393516 + 0.004326966998632997, + 0.004294172991649248, + 0.004471345993806608, + 0.004371547998744063, + 0.0044424379884731025 ], - "stddev": 0.0003097769371795639, - "iqr": 0.00028425949858501554, - "noise_ratio": 0.05601187805852866, + "stddev": 7.497403988753363e-05, + "iqr": 0.00011547098984010518, + "noise_ratio": 0.01715045560727539, "noisy": false }, { @@ -124,31 +150,35 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "read_heavy", - "time": 0.0045728120021522045, + "time": 0.005011429006117396, "times": [ - 0.004827670054510236, - 0.0035369839752092957, - 0.0045728120021522045 + 0.005175757993129082, + 0.005084271993837319, + 0.005011429006117396, + 0.0049597659963183105, + 0.004917177007882856 ], - "stddev": 0.0006835889217399349, - "iqr": 0.00064534303965047, - "noise_ratio": 0.1494898371982496, - "noisy": true + "stddev": 0.00010270960981180456, + "iqr": 0.00012450599751900882, + "noise_ratio": 0.02049507429645877, + "noisy": false }, { "driver": "sqlite", "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "read_heavy", - "time": 0.00874530605506152, + "time": 0.006095218996051699, "times": [ - 0.007777438033372164, - 0.008815814973786473, - 0.00874530605506152 + 0.006095218996051699, + 0.006120414996985346, + 0.006083756001316942, + 0.006076361009036191, + 0.006195608002599329 ], - "stddev": 0.0005802250548153436, - "iqr": 0.0005191884702071548, - "noise_ratio": 0.06634702675494437, + "stddev": 4.843750639106733e-05, + "iqr": 3.6658995668403804e-05, + "noise_ratio": 0.007946803293276862, "noisy": false }, { @@ -156,15 +186,17 @@ "library": "raw", "library_key": "raw", "scenario": "iterative_inserts", - "time": 0.006263092043809593, + "time": 0.004181689990218729, "times": [ - 0.006263092043809593, - 0.006623685942031443, - 0.005627415026538074 + 0.004181689990218729, + 0.00413410400506109, + 0.004149139000219293, + 0.004257650987710804, + 0.004246014999807812 ], - "stddev": 0.0005044252450033339, - "iqr": 0.0004981354577466846, - "noise_ratio": 0.08053933128795467, + "stddev": 5.592001368855386e-05, + "iqr": 9.687599958851933e-05, + "noise_ratio": 0.013372587116537753, "noisy": false }, { @@ -172,31 +204,35 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "iterative_inserts", - "time": 0.007394094951450825, + "time": 0.009428682998986915, "times": [ - 0.0071521460777148604, - 0.007394094951450825, - 0.011221560998819768 + 0.009358828014228493, + 0.00932132700108923, + 0.009531204996164888, + 0.009428682998986915, + 0.009545498003717512 ], - "stddev": 0.002282840825279452, - "iqr": 0.002034707460552454, - "noise_ratio": 0.3087383703169144, - "noisy": true + "stddev": 0.00010025793499290344, + "iqr": 0.00017237698193639517, + "noise_ratio": 0.010633291521591709, + "noisy": false }, { "driver": "sqlite", "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "iterative_inserts", - "time": 0.05394256697036326, + "time": 0.028197134990477934, "times": [ - 0.04960546805523336, - 0.05394256697036326, - 0.05660546000581235 + 0.028197134990477934, + 0.02821979699365329, + 0.028157600012491457, + 0.02819776799879037, + 0.027938569997786544 ], - "stddev": 0.003533207080119393, - "iqr": 0.003499995975289494, - "noise_ratio": 0.06549942426841834, + "stddev": 0.00011600569371612893, + "iqr": 4.0167986298911273e-05, + "noise_ratio": 0.004114095058072517, "noisy": false }, { @@ -204,31 +240,35 @@ "library": "raw", "library_key": "raw", "scenario": "repeated_queries", - "time": 0.16202167002484202, + "time": 0.10908412099524867, "times": [ - 0.12611343897879124, - 0.16344798903446645, - 0.16202167002484202 + 0.10908412099524867, + 0.10999828499916475, + 0.10901488899253309, + 0.10906870799954049, + 0.10911709500942379 ], - "stddev": 0.02115539360160705, - "iqr": 0.018667275027837604, - "noise_ratio": 0.13057138343508862, - "noisy": true + "stddev": 0.00041624358869188675, + "iqr": 4.838700988329947e-05, + "noise_ratio": 0.0038158036650450425, + "noisy": false }, { "driver": "sqlite", "library": "sqlspec", "library_key": "sqlspec", "scenario": "repeated_queries", - "time": 0.21340494696050882, + "time": 0.11235622799722478, "times": [ - 0.2195192320505157, - 0.19613285595551133, - 0.21340494696050882 + 0.1121271220035851, + 0.112780675990507, + 0.11174950200074818, + 0.11350953599321656, + 0.11235622799722478 ], - "stddev": 0.012128700100064229, - "iqr": 0.01169318804750219, - "noise_ratio": 0.056834203109212264, + "stddev": 0.0006746834775724083, + "iqr": 0.0006535539869219065, + "noise_ratio": 0.006004860519072188, "noisy": false }, { @@ -236,47 +276,53 @@ "library": "sqlalchemy", "library_key": "sqlalchemy", "scenario": "repeated_queries", - "time": 0.28744301199913025, + "time": 0.16552351499558426, "times": [ - 0.28744301199913025, - 0.31480474793352187, - 0.22830721095670015 + 0.1641665240022121, + 0.16491104500892106, + 0.16918576699390542, + 0.17900026100687683, + 0.16552351499558426 ], - "stddev": 0.044210729205570856, - "iqr": 0.04324876848841086, - "noise_ratio": 0.1538069368884314, - "noisy": true + "stddev": 0.006148746743329162, + "iqr": 0.004274721984984353, + "noise_ratio": 0.037147270244310575, + "noisy": false }, { "driver": "sqlite", "library": "raw", "library_key": "raw", "scenario": "dict_key_transform", - "time": 0.015097331022843719, + "time": 0.009565733998897485, "times": [ - 0.010837384033948183, - 0.015097331022843719, - 0.01851541001815349 + 0.009551441995427012, + 0.009336190996691585, + 0.009565733998897485, + 0.009962566997273825, + 0.009776612991117872 ], - "stddev": 0.003846697617881903, - "iqr": 0.003839012992102653, - "noise_ratio": 0.2547932221967895, - "noisy": true + "stddev": 0.0002389347908709586, + "iqr": 0.00022517099569085985, + "noise_ratio": 0.024978197271479367, + "noisy": false }, { "driver": "sqlite", "library": "sqlspec", "library_key": "sqlspec", "scenario": "dict_key_transform", - "time": 0.03478679293766618, + "time": 0.01544887199997902, "times": [ - 0.036578124971129, - 0.03478679293766618, - 0.03183783404529095 + 0.015325487009249628, + 0.01544887199997902, + 0.015402306002215482, + 0.015676584996981546, + 0.015492282997001894 ], - "stddev": 0.0023935882082698765, - "iqr": 0.0023701454629190266, - "noise_ratio": 0.06880738366882236, + "stddev": 0.0001314180127890617, + "iqr": 8.997699478641152e-05, + "noise_ratio": 0.008506641312662839, "noisy": false }, { @@ -284,47 +330,53 @@ "library": "raw", "library_key": "raw", "scenario": "schema_mapping", - "time": 0.009644347941502929, + "time": 0.00957132400071714, "times": [ - 0.015227310010232031, - 0.009596061077900231, - 0.009644347941502929 + 0.009600805002264678, + 0.00957132400071714, + 0.00926685800368432, + 0.00946096501138527, + 0.010403750988189131 ], - "stddev": 0.0032373538994048975, - "iqr": 0.0028156244661659002, - "noise_ratio": 0.335673693964675, - "noisy": true + "stddev": 0.00043551860653332256, + "iqr": 0.00013983999087940902, + "noise_ratio": 0.04550244109390623, + "noisy": false }, { "driver": "sqlite", "library": "sqlspec", "library_key": "sqlspec", "scenario": "schema_mapping", - "time": 0.015353130991570652, + "time": 0.013286437999340706, "times": [ - 0.015353130991570652, - 0.0200001330813393, - 0.01482859393581748 + 0.013902839011279866, + 0.01326507500198204, + 0.013467296987073496, + 0.013106648999382742, + 0.013286437999340706 ], - "stddev": 0.0028464769065998335, - "iqr": 0.00258576957276091, - "noise_ratio": 0.1854004182054226, - "noisy": true + "stddev": 0.0003059296050691119, + "iqr": 0.00020222198509145528, + "noise_ratio": 0.023025705240508603, + "noisy": false }, { "driver": "sqlite", "library": "raw", "library_key": "raw", "scenario": "complex_parameters", - "time": 0.009979208000004292, + "time": 0.00826842499373015, "times": [ - 0.010322440066374838, - 0.009979208000004292, - 0.00932889198884368 + 0.008301143010612577, + 0.008119215999613516, + 0.008257767010945827, + 0.00826842499373015, + 0.00859405800292734 ], - "stddev": 0.0005046214667323595, - "iqr": 0.0004967740387655795, - "noise_ratio": 0.05056728617462854, + "stddev": 0.00017435958410293485, + "iqr": 4.337599966675043e-05, + "noise_ratio": 0.021087399865772464, "noisy": false }, { @@ -332,15 +384,17 @@ "library": "sqlspec", "library_key": "sqlspec", "scenario": "complex_parameters", - "time": 0.007886267034336925, + "time": 0.009813410986680537, "times": [ - 0.007710185018368065, - 0.008266879012808204, - 0.007886267034336925 + 0.00970755000889767, + 0.00992931799555663, + 0.009813410986680537, + 0.009962814001482911, + 0.009700522001367062 ], - "stddev": 0.0002845401430229707, - "iqr": 0.00027834699722006917, - "noise_ratio": 0.036080460093993606, + "stddev": 0.00012172982103064316, + "iqr": 0.00022176798665896058, + "noise_ratio": 0.012404435236215375, "noisy": false }, { @@ -348,64 +402,252 @@ "library": "raw", "library_key": "raw", "scenario": "thin_path_stress", - "time": 0.025907220086082816, + "time": 0.018350698010181077, "times": [ - 0.02120684494730085, - 0.03086753294337541, - 0.025907220086082816 + 0.018361185997491702, + 0.018097220003255643, + 0.018584149991511367, + 0.018350698010181077, + 0.01830038399202749 ], - "stddev": 0.004830926802816081, - "iqr": 0.004830343998037279, - "noise_ratio": 0.18647028846646585, - "noisy": true + "stddev": 0.0001736922365496957, + "iqr": 6.0802005464211106e-05, + "noise_ratio": 0.009465156935901305, + "noisy": false }, { "driver": "sqlite", "library": "sqlspec", "library_key": "sqlspec", "scenario": "thin_path_stress", - "time": 0.0827907930361107, + "time": 0.0683942119940184, "times": [ - 0.0827907930361107, - 0.12025135790463537, - 0.07790373300667852 + 0.0683942119940184, + 0.06859845398867037, + 0.06788588200288359, + 0.06728459399892017, + 0.07255751600314397 ], - "stddev": 0.023167860678456675, - "iqr": 0.021173812448978424, - "noise_ratio": 0.27983619710408614, - "noisy": true + "stddev": 0.0020828111225034587, + "iqr": 0.0007125719857867807, + "noise_ratio": 0.030453031942024807, + "noisy": false }, { "driver": "sqlite", "library": "raw", "library_key": "raw", "scenario": "schema_type_numpy", - "time": 0.025361009989865124, + "time": 0.051206131000071764, "times": [ - 0.01786811393685639, - 0.025361009989865124, - 0.026038011070340872 + 0.050969773990800604, + 0.05114704099833034, + 0.051770587990176864, + 0.051206131000071764, + 0.051988815001095645 ], - "stddev": 0.0045341122044614416, - "iqr": 0.004084948566742241, - "noise_ratio": 0.17878279320395296, - "noisy": true + "stddev": 0.0004385630392862737, + "iqr": 0.0006235469918465242, + "noise_ratio": 0.008564658776615228, + "noisy": false }, { "driver": "sqlite", "library": "sqlspec", "library_key": "sqlspec", "scenario": "schema_type_numpy", - "time": 0.0007123140385374427, + "time": 0.03937012600363232, "times": [ - 0.0007123140385374427, - 0.0006974450079724193, - 0.0010529460851103067 + 0.039747274000546895, + 0.03891997299797367, + 0.039939324007718824, + 0.03937012600363232, + 0.039315785994403996 ], - "stddev": 0.00020109379862332885, - "iqr": 0.00017775053856894374, - "noise_ratio": 0.28231059300224415, - "noisy": true + "stddev": 0.00039773443787016765, + "iqr": 0.0004314880061428994, + "noise_ratio": 0.010102442593998107, + "noisy": false + }, + { + "driver": "sqlite", + "library": "raw", + "library_key": "raw", + "scenario": "scalar_param_select", + "time": 0.0013580560043919832, + "times": [ + 0.0013478469918482006, + 0.0013622740079881623, + 0.0013550139992730692, + 0.0013580560043919832, + 0.0013764250034000725 + ], + "stddev": 1.0621408665912961e-05, + "iqr": 7.260008715093136e-06, + "noise_ratio": 0.007821038772748024, + "noisy": false + }, + { + "driver": "sqlite", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "scalar_param_select", + "time": 0.007731876001344062, + "times": [ + 0.007662418996915221, + 0.007734676008112729, + 0.007731876001344062, + 0.00781970500247553, + 0.007689688005484641 + ], + "stddev": 5.966058487248296e-05, + "iqr": 4.4988002628088e-05, + "noise_ratio": 0.007716184902876342, + "noisy": false + }, + { + "driver": "sqlite", + "library": "raw", + "library_key": "raw", + "scenario": "no_param_select", + "time": 0.0013393950066529214, + "times": [ + 0.001320764989941381, + 0.0013500049972208217, + 0.0013393950066529214, + 0.0013467960088746622, + 0.001333893000264652 + ], + "stddev": 1.1588819270037595e-05, + "iqr": 1.2903008610010147e-05, + "noise_ratio": 0.008652278986015822, + "noisy": false + }, + { + "driver": "sqlite", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "no_param_select", + "time": 0.0053518780041486025, + "times": [ + 0.005385716998716816, + 0.0053518780041486025, + 0.005310234002536163, + 0.005422768997959793, + 0.005300614007865079 + ], + "stddev": 5.1246655623517794e-05, + "iqr": 7.548299618065357e-05, + "noise_ratio": 0.009575452875381883, + "noisy": false + }, + { + "driver": "sqlite", + "library": "raw", + "library_key": "raw", + "scenario": "complex_query", + "time": 0.0030574999982491136, + "times": [ + 0.003050535000511445, + 0.0030785599956288934, + 0.0030124610057100654, + 0.003082666007685475, + 0.0030574999982491136 + ], + "stddev": 2.804994003750898e-05, + "iqr": 2.802499511744827e-05, + "noise_ratio": 0.009174142290620396, + "noisy": false + }, + { + "driver": "sqlite", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "complex_query", + "time": 0.006443192993174307, + "times": [ + 0.0064240339997923, + 0.006305467002675869, + 0.006445741004426964, + 0.006443192993174307, + 0.006471240005339496 + ], + "stddev": 6.507459549662343e-05, + "iqr": 2.1707004634663463e-05, + "noise_ratio": 0.010099743336193899, + "noisy": false + }, + { + "driver": "sqlite", + "library": "raw", + "library_key": "raw", + "scenario": "session_per_request", + "time": 0.010022154005127959, + "times": [ + 0.010022154005127959, + 0.010339030995965004, + 0.01021236200176645, + 0.009826769004575908, + 0.009880614001303911 + ], + "stddev": 0.00021737530705022898, + "iqr": 0.0003317480004625395, + "noise_ratio": 0.021689479820306713, + "noisy": false + }, + { + "driver": "sqlite", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "session_per_request", + "time": 0.02374547498766333, + "times": [ + 0.023479077994124964, + 0.02374547498766333, + 0.0237338339939015, + 0.027480500008095987, + 0.024171199009288102 + ], + "stddev": 0.0016723984789667789, + "iqr": 0.00043736501538660377, + "noise_ratio": 0.07043019690427979, + "noisy": false + }, + { + "driver": "sqlite", + "library": "raw", + "library_key": "raw", + "scenario": "observability_select", + "time": 0.017145584992249496, + "times": [ + 0.017244325004867278, + 0.017175964007037692, + 0.017145584992249496, + 0.01694815600058064, + 0.017087613989133388 + ], + "stddev": 0.00011160414647370428, + "iqr": 8.835001790430397e-05, + "noise_ratio": 0.006509206103154481, + "noisy": false + }, + { + "driver": "sqlite", + "library": "sqlspec", + "library_key": "sqlspec", + "scenario": "observability_select", + "time": 0.035870573003194295, + "times": [ + 0.03587337300996296, + 0.035870573003194295, + 0.03573423300986178, + 0.036352101000375114, + 0.03557527500379365 + ], + "stddev": 0.0002903064358924952, + "iqr": 0.00013914000010117888, + "noise_ratio": 0.008093164161794774, + "noisy": false } ] -} +} \ No newline at end of file diff --git a/tools/perf_baselines/subsystems.json b/tools/perf_baselines/subsystems.json new file mode 100644 index 000000000..11610c060 --- /dev/null +++ b/tools/perf_baselines/subsystems.json @@ -0,0 +1,336 @@ +{ + "included_cases": [ + "SQL() construction (no params)", + "SQL() construction (with params)", + "SQL() construction (SELECT + param)", + "QueryBuilder.to_statement() - same shape varying values", + "SQL.compile() - INSERT", + "SQL.compile() - SELECT", + "SQL.compile() - complex JOIN", + "QC _cached_execution() - cache miss", + "QC _cached_execution() - full cycle", + "prepare_driver_parameters (tuple)", + "prepare_driver_parameters (dict)", + "_driver_parameters (3 params)", + "SQLResult.__init__() - DML", + "SQLResult.__init__() - SELECT (100 rows)", + "StorageRegistry.get() - cached alias", + "SyncStoragePipeline.write_rows() - local jsonl", + "_decode_arrow_payload() - jsonl", + "SqliteCursor context manager", + "raw sqlite3 cursor create+close", + "session.execute() - full path", + "raw sqlite3 conn.execute()", + "stub driver: fast-path execute(SELECT, tuple)", + "stub driver: fast-path execute(INSERT, tuple)", + "stub driver: fast-path fetch_one_or_none(SELECT, tuple)", + "stub driver: parameterless execute(SELECT)", + "stub driver: scalar-param execute(SELECT, int)", + "stub driver: complex CTE+2-join execute(SELECT, int)", + "stub driver: session-per-request simple SELECT", + "stub driver: session-per-request complex CTE SELECT", + "stub driver: execute_many 100 rows", + "librt ParameterConverter baseline", + "librt ParameterConverter StringWriter", + "librt splitter join baseline", + "librt splitter StringWriter", + "librt psqlpy copy baseline", + "librt psqlpy copy StringWriter" + ], + "metadata": { + "iterations": 5000, + "profile": false, + "timestamp": "2026-10-13T21:39:22+0000", + "warmup": 100 + }, + "results": [ + { + "description": "Time SQL('INSERT INTO test (value) VALUES (?)')", + "iterations": 5000, + "name": "SQL() construction (no params)", + "ops_per_sec": 913719.3186319757, + "time_per_op_us": 1.0944279929390177, + "total_time": 0.005437102998257615 + }, + { + "description": "Time SQL('INSERT ...', ('hello',))", + "iterations": 5000, + "name": "SQL() construction (with params)", + "ops_per_sec": 626649.6571693261, + "time_per_op_us": 1.5957879950292408, + "total_time": 0.008684266998898238 + }, + { + "description": "Time SQL('SELECT ... WHERE ...', param)", + "iterations": 5000, + "name": "SQL() construction (SELECT + param)", + "ops_per_sec": 637129.3471429019, + "time_per_op_us": 1.5695400070399046, + "total_time": 0.007883617014158517 + }, + { + "description": "Convert same-shape UPDATE builders while parameter values vary", + "iterations": 5000, + "name": "QueryBuilder.to_statement() - same shape varying values", + "ops_per_sec": 4120.919565822979, + "time_per_op_us": 242.66428500413895, + "total_time": 1.2694333770195954 + }, + { + "description": "Compile simple INSERT statement", + "iterations": 5000, + "name": "SQL.compile() - INSERT", + "ops_per_sec": 161767.66763378118, + "time_per_op_us": 6.1817050009267405, + "total_time": 0.030903736987966113 + }, + { + "description": "Compile simple SELECT statement", + "iterations": 5000, + "name": "SQL.compile() - SELECT", + "ops_per_sec": 162352.42156694084, + "time_per_op_us": 6.159440003102645, + "total_time": 0.03080166499421466 + }, + { + "description": "Compile complex JOIN with multiple params", + "iterations": 5000, + "name": "SQL.compile() - complex JOIN", + "ops_per_sec": 147818.3926397272, + "time_per_op_us": 6.76505800220184, + "total_time": 0.033716250996803865 + }, + { + "description": "Cache lookup with cache miss (early return, no execution)", + "iterations": 5000, + "name": "QC _cached_execution() - cache miss", + "ops_per_sec": 3329193.936325096, + "time_per_op_us": 0.3003730089403689, + "total_time": 0.0015094660047907382 + }, + { + "description": "Full stmt_cache lookup -> prepare -> execute cycle", + "iterations": 5000, + "name": "QC _cached_execution() - full cycle", + "ops_per_sec": 48080.33261572494, + "time_per_op_us": 20.79852500173729, + "total_time": 0.10446592500375118 + }, + { + "description": "Prepare a single positional parameter tuple", + "iterations": 5000, + "name": "prepare_driver_parameters (tuple)", + "ops_per_sec": 1000033.0009910618, + "time_per_op_us": 0.9999670000979677, + "total_time": 0.005002377001801506 + }, + { + "description": "Prepare a single named parameter dict", + "iterations": 5000, + "name": "prepare_driver_parameters (dict)", + "ops_per_sec": 854326.9969933727, + "time_per_op_us": 1.170511997770518, + "total_time": 0.005851471985806711 + }, + { + "description": "Format a 3-element positional parameter set", + "iterations": 5000, + "name": "_driver_parameters (3 params)", + "ops_per_sec": 635211.826417988, + "time_per_op_us": 1.574277994222939, + "total_time": 0.007899189979070798 + }, + { + "description": "Construct SQLResult for an INSERT operation", + "iterations": 5000, + "name": "SQLResult.__init__() - DML", + "ops_per_sec": 1198160.0977602866, + "time_per_op_us": 0.834613005281426, + "total_time": 0.004188569990219548 + }, + { + "description": "Construct SQLResult for a SELECT with 100 rows", + "iterations": 5000, + "name": "SQLResult.__init__() - SELECT (100 rows)", + "ops_per_sec": 1097976.8669275937, + "time_per_op_us": 0.910766000743024, + "total_time": 0.004547191012534313 + }, + { + "description": "Resolve a cached storage alias through the registry hot path", + "iterations": 5000, + "name": "StorageRegistry.get() - cached alias", + "ops_per_sec": 980211.4838925864, + "time_per_op_us": 1.020188006805256, + "total_time": 0.005175599013455212 + }, + { + "description": "Encode rows and route them through the sync local storage pipeline", + "iterations": 5000, + "name": "SyncStoragePipeline.write_rows() - local jsonl", + "ops_per_sec": 5791.079632721365, + "time_per_op_us": 172.6793730049394, + "total_time": 0.8718435269984184 + }, + { + "description": "Decode JSONL bytes through the Arrow boundary helper", + "iterations": 5000, + "name": "_decode_arrow_payload() - jsonl", + "ops_per_sec": 6869.321496958173, + "time_per_op_us": 145.57478499773424, + "total_time": 0.7582230610132683 + }, + { + "description": "Enter/exit SqliteCursor context manager", + "iterations": 5000, + "name": "SqliteCursor context manager", + "ops_per_sec": 1128520.4166203288, + "time_per_op_us": 0.8861160022206604, + "total_time": 0.004822624992812052 + }, + { + "description": "Raw sqlite3 cursor() + close() for comparison", + "iterations": 5000, + "name": "raw sqlite3 cursor create+close", + "ops_per_sec": 5109861.88069036, + "time_per_op_us": 0.19570000586099923, + "total_time": 0.0009786640148377046 + }, + { + "description": "Full sqlspec execute() including QC, dispatch, result", + "iterations": 5000, + "name": "session.execute() - full path", + "ops_per_sec": 46879.57367445652, + "time_per_op_us": 21.331252006348222, + "total_time": 0.10676726199744735 + }, + { + "description": "Raw sqlite3 execute for comparison baseline", + "iterations": 5000, + "name": "raw sqlite3 conn.execute()", + "ops_per_sec": 62133.80282324967, + "time_per_op_us": 16.09429898962844, + "total_time": 0.0820974859962007 + }, + { + "description": "Pure-Python driver overhead on warm fast-path SELECT with 1-tuple param", + "iterations": 5000, + "name": "stub driver: fast-path execute(SELECT, tuple)", + "ops_per_sec": 199214.29866762075, + "time_per_op_us": 5.019720003474504, + "total_time": 0.02853145000699442 + }, + { + "description": "Pure-Python driver overhead on warm fast-path INSERT with 1-tuple param", + "iterations": 5000, + "name": "stub driver: fast-path execute(INSERT, tuple)", + "ops_per_sec": 266848.62190703605, + "time_per_op_us": 3.747442999156192, + "total_time": 0.021824130992172286 + }, + { + "description": "Pure-Python driver overhead on warm fetch_one_or_none with 1-tuple param", + "iterations": 5000, + "name": "stub driver: fast-path fetch_one_or_none(SELECT, tuple)", + "ops_per_sec": 152632.9798998559, + "time_per_op_us": 6.551664002472535, + "total_time": 0.03708145199925639 + }, + { + "description": "Pure-Python driver overhead on parameterless SELECT 1", + "iterations": 5000, + "name": "stub driver: parameterless execute(SELECT)", + "ops_per_sec": 238372.71592313284, + "time_per_op_us": 4.195110988803208, + "total_time": 0.021016020997194573 + }, + { + "description": "Pure-Python driver overhead on SELECT with scalar positional param", + "iterations": 5000, + "name": "stub driver: scalar-param execute(SELECT, int)", + "ops_per_sec": 191849.39328270138, + "time_per_op_us": 5.21242200920824, + "total_time": 0.026079890012624674 + }, + { + "description": "Pure-Python driver overhead on CTE + 2-join query with scalar param", + "iterations": 2000, + "name": "stub driver: complex CTE+2-join execute(SELECT, int)", + "ops_per_sec": 190476.18934846655, + "time_per_op_us": 5.250000031082891, + "total_time": 0.0105269009800395 + }, + { + "description": "New driver instance per request executing simple SELECT", + "iterations": 2000, + "name": "stub driver: session-per-request simple SELECT", + "ops_per_sec": 150011.68233821707, + "time_per_op_us": 6.666147492069285, + "total_time": 0.013450521000777371 + }, + { + "description": "New driver instance per request executing CTE + 2-join SELECT", + "iterations": 2000, + "name": "stub driver: session-per-request complex CTE SELECT", + "ops_per_sec": 148770.50532131383, + "time_per_op_us": 6.721762474626303, + "total_time": 0.013813848985591903 + }, + { + "description": "Pure-Python driver overhead on execute_many with 100 parameter rows", + "iterations": 2000, + "name": "stub driver: execute_many 100 rows", + "ops_per_sec": 7149.427723435246, + "time_per_op_us": 139.87133497721516, + "total_time": 0.2836982590088155 + }, + { + "description": "Current list-join placeholder assembly shape from ParameterConverter", + "iterations": 5000, + "name": "librt ParameterConverter baseline", + "ops_per_sec": 1007553.6268856531, + "time_per_op_us": 0.9925030026352033, + "total_time": 0.00521571101853624 + }, + { + "description": "librt.strings.StringWriter candidate for ParameterConverter assembly", + "iterations": 5000, + "name": "librt ParameterConverter StringWriter", + "ops_per_sec": 1062117.9553103049, + "time_per_op_us": 0.941515012527816, + "total_time": 0.004716872004792094 + }, + { + "description": "Current list-join statement assembly shape from StatementSplitter", + "iterations": 5000, + "name": "librt splitter join baseline", + "ops_per_sec": 341651.7555122614, + "time_per_op_us": 2.9269570077303797, + "total_time": 0.014837033028015867 + }, + { + "description": "librt.strings.StringWriter candidate for StatementSplitter assembly", + "iterations": 5000, + "name": "librt splitter StringWriter", + "ops_per_sec": 302831.35208128975, + "time_per_op_us": 3.302167999208905, + "total_time": 0.020603441007551737 + }, + { + "description": "Current StringIO row assembly shape from psqlpy binary copy encoding", + "iterations": 5000, + "name": "librt psqlpy copy baseline", + "ops_per_sec": 60645.925178129015, + "time_per_op_us": 16.48915400437545, + "total_time": 0.08254189301806036 + }, + { + "description": "librt.strings.StringWriter candidate for psqlpy binary copy encoding", + "iterations": 5000, + "name": "librt psqlpy copy StringWriter", + "ops_per_sec": 90935.46218894077, + "time_per_op_us": 10.99681000050623, + "total_time": 0.05711644601251464 + } + ] +} \ No newline at end of file diff --git a/tools/scripts/bench.py b/tools/scripts/bench.py index 368f0308b..f95ef1351 100644 --- a/tools/scripts/bench.py +++ b/tools/scripts/bench.py @@ -7,9 +7,11 @@ import cProfile import gc import importlib +import importlib.metadata import inspect import json import os +import platform import pstats import sqlite3 import statistics @@ -26,7 +28,7 @@ from rich.console import Console from rich.table import Table -from sqlspec import SQLSpec +from sqlspec import ObservabilityConfig, SQLSpec from sqlspec.adapters.duckdb import DuckDBConfig from sqlspec.adapters.sqlite import SqliteConfig from sqlspec.utils.schema import _convert_numpy_recursive, to_schema, transform_dict_keys @@ -36,36 +38,77 @@ "main", "print_benchmark_table", "raw_adbc_rows", + "raw_aiosqlite_complex_query", + "raw_aiosqlite_no_param_select", + "raw_aiosqlite_observability_select", + "raw_aiosqlite_scalar_param_select", + "raw_aiosqlite_session_per_request", "raw_aiosqlite_worker_hops", + "raw_asyncpg_complex_query", "raw_asyncpg_initialization", "raw_asyncpg_iterative_inserts", + "raw_asyncpg_no_param_select", + "raw_asyncpg_observability_select", "raw_asyncpg_read_heavy", "raw_asyncpg_repeated_queries", "raw_asyncpg_rows", + "raw_asyncpg_scalar_param_select", + "raw_asyncpg_session_per_request", "raw_asyncpg_write_heavy", "raw_cockroach_asyncpg_rows", "raw_cockroach_psycopg_async_rows", "raw_cockroach_psycopg_sync_rows", "raw_duckdb_bulk", + "raw_duckdb_complex_query", "raw_duckdb_initialization", "raw_duckdb_iterative_inserts", + "raw_duckdb_no_param_select", + "raw_duckdb_observability_select", "raw_duckdb_read_heavy", "raw_duckdb_repeated_queries", + "raw_duckdb_scalar_param_select", + "raw_duckdb_session_per_request", "raw_duckdb_write_heavy", "raw_mysqlconnector_json_rows", + "raw_oracle_complex_query", "raw_oracle_lob_fetch_1k", "raw_oracle_lob_fetch_100k", + "raw_oracle_no_param_select", + "raw_oracle_observability_select", + "raw_oracle_scalar_param_select", + "raw_oracle_session_per_request", + "raw_psqlpy_complex_query", + "raw_psqlpy_no_param_select", + "raw_psqlpy_observability_select", + "raw_psqlpy_rows", + "raw_psqlpy_scalar_param_select", + "raw_psqlpy_session_per_request", + "raw_psycopg_async_complex_query", + "raw_psycopg_async_no_param_select", + "raw_psycopg_async_observability_select", "raw_psycopg_async_rows", + "raw_psycopg_async_scalar_param_select", + "raw_psycopg_async_session_per_request", + "raw_psycopg_sync_complex_query", + "raw_psycopg_sync_no_param_select", + "raw_psycopg_sync_observability_select", "raw_psycopg_sync_rows", + "raw_psycopg_sync_scalar_param_select", + "raw_psycopg_sync_session_per_request", "raw_spanner_strings", "raw_sqlite_complex_parameters", + "raw_sqlite_complex_query", "raw_sqlite_dict_key_transform", "raw_sqlite_initialization", "raw_sqlite_iterative_inserts", + "raw_sqlite_no_param_select", + "raw_sqlite_observability_select", "raw_sqlite_read_heavy", "raw_sqlite_repeated_queries", + "raw_sqlite_scalar_param_select", "raw_sqlite_schema_mapping", "raw_sqlite_schema_type_numpy", + "raw_sqlite_session_per_request", "raw_sqlite_thin_path_stress", "raw_sqlite_write_heavy", "run_benchmark", @@ -86,23 +129,44 @@ "sqlalchemy_sqlite_repeated_queries", "sqlalchemy_sqlite_write_heavy", "sqlspec_adbc_rows", + "sqlspec_aiosqlite_complex_query", + "sqlspec_aiosqlite_no_param_select", + "sqlspec_aiosqlite_observability_select", + "sqlspec_aiosqlite_scalar_param_select", + "sqlspec_aiosqlite_session_per_request", "sqlspec_aiosqlite_worker_hops", + "sqlspec_asyncpg_complex_query", "sqlspec_asyncpg_initialization", "sqlspec_asyncpg_iterative_inserts", + "sqlspec_asyncpg_no_param_select", + "sqlspec_asyncpg_observability_select", "sqlspec_asyncpg_read_heavy", "sqlspec_asyncpg_repeated_queries", "sqlspec_asyncpg_rows", + "sqlspec_asyncpg_scalar_param_select", + "sqlspec_asyncpg_session_per_request", "sqlspec_asyncpg_write_heavy", "sqlspec_cockroach_asyncpg_rows", "sqlspec_cockroach_psycopg_async_rows", "sqlspec_cockroach_psycopg_sync_rows", "sqlspec_duckdb_bulk", + "sqlspec_duckdb_complex_query", "sqlspec_duckdb_initialization", "sqlspec_duckdb_iterative_inserts", + "sqlspec_duckdb_no_param_select", + "sqlspec_duckdb_observability_select", "sqlspec_duckdb_read_heavy", "sqlspec_duckdb_repeated_queries", + "sqlspec_duckdb_scalar_param_select", + "sqlspec_duckdb_session_per_request", "sqlspec_duckdb_write_heavy", "sqlspec_mysqlconnector_json_rows", + "sqlspec_oracle_async_complex_query", + "sqlspec_oracle_async_no_param_select", + "sqlspec_oracle_async_observability_select", + "sqlspec_oracle_async_scalar_param_select", + "sqlspec_oracle_async_session_per_request", + "sqlspec_oracle_complex_query", "sqlspec_oracle_json_read", "sqlspec_oracle_lob_fetch_1k", "sqlspec_oracle_lob_fetch_100k", @@ -113,62 +177,75 @@ "sqlspec_oracle_lob_fetch_fetch_lobs_true_1k", "sqlspec_oracle_lob_fetch_fetch_lobs_true_100k", "sqlspec_oracle_native_json_write", + "sqlspec_oracle_no_param_select", + "sqlspec_oracle_observability_select", + "sqlspec_oracle_scalar_param_select", "sqlspec_oracle_serialized_json_write", + "sqlspec_oracle_session_per_request", + "sqlspec_psqlpy_complex_query", + "sqlspec_psqlpy_no_param_select", + "sqlspec_psqlpy_observability_select", + "sqlspec_psqlpy_rows", + "sqlspec_psqlpy_scalar_param_select", + "sqlspec_psqlpy_session_per_request", + "sqlspec_psycopg_async_complex_query", + "sqlspec_psycopg_async_no_param_select", + "sqlspec_psycopg_async_observability_select", "sqlspec_psycopg_async_rows", + "sqlspec_psycopg_async_scalar_param_select", + "sqlspec_psycopg_async_session_per_request", + "sqlspec_psycopg_sync_complex_query", + "sqlspec_psycopg_sync_no_param_select", + "sqlspec_psycopg_sync_observability_select", "sqlspec_psycopg_sync_rows", + "sqlspec_psycopg_sync_scalar_param_select", + "sqlspec_psycopg_sync_session_per_request", "sqlspec_spanner_strings", "sqlspec_sqlite_complex_parameters", + "sqlspec_sqlite_complex_query", "sqlspec_sqlite_dict_key_transform", "sqlspec_sqlite_initialization", "sqlspec_sqlite_iterative_inserts", + "sqlspec_sqlite_no_param_select", + "sqlspec_sqlite_observability_select", "sqlspec_sqlite_read_heavy", "sqlspec_sqlite_repeated_queries", + "sqlspec_sqlite_scalar_param_select", "sqlspec_sqlite_schema_mapping", "sqlspec_sqlite_schema_type_numpy", + "sqlspec_sqlite_session_per_request", "sqlspec_sqlite_thin_path_stress", "sqlspec_sqlite_write_heavy", ) -# Pool leak detection helper _leaked_pools: list[str] = [] +ROWS_TO_INSERT = 10_000 -class BenchmarkUnavailableError(RuntimeError): - """Signal that an optional benchmark dependency or service is unavailable.""" - - -def _benchmark_unavailable() -> NoReturn: - raise BenchmarkUnavailableError - - -def _is_compiled() -> bool: - """Detect if sqlspec driver modules are mypyc-compiled.""" - try: - from sqlspec.driver import _sync - - return hasattr(_sync, "__file__") and (_sync.__file__ or "").endswith(".so") - except ImportError: - return False - - -SQLSPEC_LABEL = "sqlspec (mypyc)" if _is_compiled() else "sqlspec" - +POOL_SIZE = 1 -ROWS_TO_INSERT = 10_000 -POOL_SIZE = 1 # Match the single raw connection used by each benchmark scenario DEFAULT_BENCH_ITERATIONS = 7 + DEFAULT_BENCH_WARMUP = 3 + NOISY_STDDEV_RATIO = 0.10 + CORE_LIBRARIES = ("raw", "sqlspec", "sqlalchemy") + CORE_SCENARIOS = ("initialization", "write_heavy", "read_heavy", "iterative_inserts", "repeated_queries") + ORACLE_LOB_ROWS = 100 + ORACLE_LOB_PAYLOAD_SIZES = {"1k": 1024, "100k": 100 * 1024} + ORACLE_JSON_ROWS = 100 + ORACLE_JSON_PAYLOAD = { "active": True, "count": 3, "items": [{"name": "alpha", "value": 1.25}, {"name": "beta", "value": 2.5}], } + ORACLE_LOB_ENV_VARS = ( "SQLSPEC_BENCH_ORACLE_HOST", "SQLSPEC_BENCH_ORACLE_PORT", @@ -176,8 +253,24 @@ def _is_compiled() -> bool: "SQLSPEC_BENCH_ORACLE_USER", "SQLSPEC_BENCH_ORACLE_PASSWORD", ) + POSTGRES_DSN_ENV = "SQLSPEC_BENCH_POSTGRES_DSN" + COCKROACH_DSN_ENV = "SQLSPEC_BENCH_COCKROACH_DSN" + +HOT_PATH_EXTENDED_SCENARIOS = ( + ("raw", "scalar_param_select"), + ("sqlspec", "scalar_param_select"), + ("raw", "no_param_select"), + ("sqlspec", "no_param_select"), + ("raw", "complex_query"), + ("sqlspec", "complex_query"), + ("raw", "session_per_request"), + ("sqlspec", "session_per_request"), + ("raw", "observability_select"), + ("sqlspec", "observability_select"), +) + SQLITE_EXTENDED_SCENARIOS = ( ("raw", "dict_key_transform"), ("sqlspec", "dict_key_transform"), @@ -189,7 +282,9 @@ def _is_compiled() -> bool: ("sqlspec", "thin_path_stress"), ("raw", "schema_type_numpy"), ("sqlspec", "schema_type_numpy"), + *HOT_PATH_EXTENDED_SCENARIOS, ) + ORACLE_EXTENDED_SCENARIOS = ( ("sqlspec_native_json", "json_write"), ("sqlspec_serialized_json", "json_write"), @@ -204,13 +299,37 @@ def _is_compiled() -> bool: ("sqlspec_fetch_lobs_true", "lob_fetch_100k"), ("sqlspec_async", "lob_fetch_100k"), ("sqlspec_async_fetch_lobs_true", "lob_fetch_100k"), + ("raw", "scalar_param_select"), + ("sqlspec", "scalar_param_select"), + ("sqlspec_async", "scalar_param_select"), + ("raw", "no_param_select"), + ("sqlspec", "no_param_select"), + ("sqlspec_async", "no_param_select"), + ("raw", "complex_query"), + ("sqlspec", "complex_query"), + ("sqlspec_async", "complex_query"), + ("raw", "session_per_request"), + ("sqlspec", "session_per_request"), + ("sqlspec_async", "session_per_request"), + ("raw", "observability_select"), + ("sqlspec", "observability_select"), + ("sqlspec_async", "observability_select"), ) -AIOSQLITE_EXTENDED_SCENARIOS = (("raw", "worker_hops"), ("sqlspec", "worker_hops")) + +AIOSQLITE_EXTENDED_SCENARIOS = (("raw", "worker_hops"), ("sqlspec", "worker_hops"), *HOT_PATH_EXTENDED_SCENARIOS) + SPANNER_EXTENDED_SCENARIOS = (("raw", "strings"), ("sqlspec", "strings")) + MYSQLCONNECTOR_EXTENDED_SCENARIOS = (("raw", "json_rows"), ("sqlspec", "json_rows")) + ADBC_EXTENDED_SCENARIOS = (("raw", "rows"), ("sqlspec", "rows")) -DUCKDB_EXTENDED_SCENARIOS = (("raw", "bulk"), ("sqlspec", "bulk")) + +DUCKDB_EXTENDED_SCENARIOS = (("raw", "bulk"), ("sqlspec", "bulk"), *HOT_PATH_EXTENDED_SCENARIOS) + SERVICE_ROWS_SCENARIOS = (("raw", "rows"), ("sqlspec", "rows")) + +POSTGRES_EXTENDED_SCENARIOS = (*SERVICE_ROWS_SCENARIOS, *HOT_PATH_EXTENDED_SCENARIOS) + EXTENDED_SCENARIOS_BY_DRIVER = { "sqlite": SQLITE_EXTENDED_SCENARIOS, "aiosqlite": AIOSQLITE_EXTENDED_SCENARIOS, @@ -219,14 +338,245 @@ def _is_compiled() -> bool: "adbc": ADBC_EXTENDED_SCENARIOS, "duckdb": DUCKDB_EXTENDED_SCENARIOS, "oracle": ORACLE_EXTENDED_SCENARIOS, - "psycopg_sync": SERVICE_ROWS_SCENARIOS, - "psycopg_async": SERVICE_ROWS_SCENARIOS, - "asyncpg": SERVICE_ROWS_SCENARIOS, + "psycopg_sync": POSTGRES_EXTENDED_SCENARIOS, + "psycopg_async": POSTGRES_EXTENDED_SCENARIOS, + "asyncpg": POSTGRES_EXTENDED_SCENARIOS, + "psqlpy": POSTGRES_EXTENDED_SCENARIOS, "cockroach_psycopg_sync": SERVICE_ROWS_SCENARIOS, "cockroach_psycopg_async": SERVICE_ROWS_SCENARIOS, "cockroach_asyncpg": SERVICE_ROWS_SCENARIOS, } +SCALAR_PARAM_SELECT_SQL = "SELECT ?;" + +SCALAR_PARAM_SELECT_ASYNCPG_SQL = "SELECT $1::int;" + +SCALAR_PARAM_SELECT_PG_SQL = "SELECT %s::int;" + +SCALAR_PARAM_SELECT_ORACLE_SQL = "SELECT :1 FROM DUAL" + +NO_PARAM_SELECT_SQL = "SELECT 1;" + +NO_PARAM_SELECT_ORACLE_SQL = "SELECT 1 FROM DUAL" + +COMPLEX_SCHEMA_DDL = ( + "CREATE TABLE users (id INTEGER, org_id INTEGER, status TEXT, name TEXT);", + "CREATE TABLE orgs (id INTEGER, tier TEXT, region TEXT);", + "CREATE TABLE events (id INTEGER, user_id INTEGER, kind TEXT, ts INTEGER, amount FLOAT);", +) + +COMPLEX_QUERY_SQL = """ +WITH recent_events AS ( + SELECT user_id, kind, COUNT(*) AS cnt, SUM(amount) AS total_amount + FROM events + WHERE ts >= ? + GROUP BY user_id, kind +) +SELECT u.id, u.name, o.tier, o.region, r.kind, r.cnt, r.total_amount +FROM users u +JOIN orgs o ON o.id = u.org_id +LEFT JOIN recent_events r ON r.user_id = u.id +WHERE u.status = 'active' AND o.region = 'us' +ORDER BY r.total_amount DESC +LIMIT 25 +""" + +POSTGRES_COMPLEX_DROP_DDL = ( + "DROP TABLE IF EXISTS sqlspec_bench_events", + "DROP TABLE IF EXISTS sqlspec_bench_users", + "DROP TABLE IF EXISTS sqlspec_bench_orgs", +) + +POSTGRES_COMPLEX_SCHEMA_DDL = ( + "CREATE TABLE sqlspec_bench_users (id INTEGER, org_id INTEGER, status TEXT, name TEXT)", + "CREATE TABLE sqlspec_bench_orgs (id INTEGER, tier TEXT, region TEXT)", + "CREATE TABLE sqlspec_bench_events (id INTEGER, user_id INTEGER, kind TEXT, ts INTEGER, amount DOUBLE PRECISION)", +) + +POSTGRES_COMPLEX_QUERY_ASYNCPG_SQL = """ +WITH recent_events AS ( + SELECT user_id, kind, COUNT(*) AS cnt, SUM(amount) AS total_amount + FROM sqlspec_bench_events + WHERE ts >= $1::int + GROUP BY user_id, kind +) +SELECT u.id, u.name, o.tier, o.region, r.kind, r.cnt, r.total_amount +FROM sqlspec_bench_users u +JOIN sqlspec_bench_orgs o ON o.id = u.org_id +LEFT JOIN recent_events r ON r.user_id = u.id +WHERE u.status = 'active' AND o.region = 'us' +ORDER BY r.total_amount DESC +LIMIT 25 +""" + +POSTGRES_COMPLEX_QUERY_PG_SQL = """ +WITH recent_events AS ( + SELECT user_id, kind, COUNT(*) AS cnt, SUM(amount) AS total_amount + FROM sqlspec_bench_events + WHERE ts >= %s::int + GROUP BY user_id, kind +) +SELECT u.id, u.name, o.tier, o.region, r.kind, r.cnt, r.total_amount +FROM sqlspec_bench_users u +JOIN sqlspec_bench_orgs o ON o.id = u.org_id +LEFT JOIN recent_events r ON r.user_id = u.id +WHERE u.status = 'active' AND o.region = 'us' +ORDER BY r.total_amount DESC +LIMIT 25 +""" + +ORACLE_COMPLEX_TABLES = ("SQLSPEC_BENCH_EVENTS", "SQLSPEC_BENCH_USERS", "SQLSPEC_BENCH_ORGS") + +ORACLE_COMPLEX_SCHEMA_DDL = ( + "CREATE TABLE SQLSPEC_BENCH_USERS (id NUMBER, org_id NUMBER, status VARCHAR2(32), name VARCHAR2(64))", + "CREATE TABLE SQLSPEC_BENCH_ORGS (id NUMBER, tier VARCHAR2(32), region VARCHAR2(32))", + "CREATE TABLE SQLSPEC_BENCH_EVENTS (id NUMBER, user_id NUMBER, kind VARCHAR2(32), ts NUMBER, amount BINARY_DOUBLE)", +) + +ORACLE_COMPLEX_QUERY_SQL = """ +WITH recent_events AS ( + SELECT user_id, kind, COUNT(*) AS cnt, SUM(amount) AS total_amount + FROM SQLSPEC_BENCH_EVENTS + WHERE ts >= :1 + GROUP BY user_id, kind +) +SELECT u.id, u.name, o.tier, o.region, r.kind, r.cnt, r.total_amount +FROM SQLSPEC_BENCH_USERS u +JOIN SQLSPEC_BENCH_ORGS o ON o.id = u.org_id +LEFT JOIN recent_events r ON r.user_id = u.id +WHERE u.status = 'active' AND o.region = 'us' +ORDER BY r.total_amount DESC +FETCH FIRST 25 ROWS ONLY +""" + +ORACLE_KV_TABLE = "SQLSPEC_BENCH_KV" + +ORACLE_KV_CREATE_SQL = f"CREATE TABLE {ORACLE_KV_TABLE} (value VARCHAR2(64))" + +ORACLE_KV_INSERT_SQL = f"INSERT INTO {ORACLE_KV_TABLE} (value) VALUES (:1)" + +ORACLE_KV_SELECT_BY_VALUE_SQL = f"SELECT value FROM {ORACLE_KV_TABLE} WHERE value = :1" + +SERVICE_ROWS_SELECT_BY_PAYLOAD_PG = "SELECT id, payload FROM sqlspec_bench_rows WHERE payload = %s" + +SERVICE_ROWS_SELECT_BY_PAYLOAD_ASYNCPG = "SELECT id, payload FROM sqlspec_bench_rows WHERE payload = $1" + +HOT_PATH_COMPLEX_ITERATIONS_CAP = 500 + +CREATE_TEST_TABLE = "CREATE TABLE test (value TEXT);" + +DROP_TEST_TABLE = "DROP TABLE IF EXISTS test;" + +INSERT_TEST_VALUE = "INSERT INTO test (value) VALUES (?);" + +INSERT_TEST_VALUE_ASYNCPG = "INSERT INTO test (value) VALUES ($1);" + +SELECT_TEST_VALUES = "SELECT * FROM test;" + +INSERT_TEST_VALUE_SQLA = "INSERT INTO test (value) VALUES (:value);" + +SELECT_BY_VALUE = "SELECT * FROM test WHERE value = ?;" + +SELECT_BY_VALUE_ASYNCPG = "SELECT * FROM test WHERE value = $1;" + +SELECT_BY_VALUE_SQLA = "SELECT * FROM test WHERE value = :value;" + +SERVICE_ROWS_CREATE = "CREATE TABLE IF NOT EXISTS sqlspec_bench_rows (id INTEGER PRIMARY KEY, payload TEXT)" + +SERVICE_ROWS_DROP = "DROP TABLE IF EXISTS sqlspec_bench_rows" + +SERVICE_ROWS_INSERT_PG = "INSERT INTO sqlspec_bench_rows (id, payload) VALUES (%s, %s)" + +SERVICE_ROWS_INSERT_ASYNCPG = "INSERT INTO sqlspec_bench_rows (id, payload) VALUES ($1, $2)" + +SERVICE_ROWS_SELECT = "SELECT id, payload FROM sqlspec_bench_rows ORDER BY id" + +CREATE_WIDE_TABLE = """CREATE TABLE wide_test ( + first_name TEXT, + last_name TEXT, + email_address TEXT, + phone_number TEXT, + street_address TEXT, + postal_code TEXT, + country_name TEXT, + birth_date TEXT, + account_balance TEXT, + is_active TEXT +);""" + +INSERT_WIDE_ROW = ( + "INSERT INTO wide_test " + "(first_name, last_name, email_address, phone_number, street_address, " + "postal_code, country_name, birth_date, account_balance, is_active) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);" +) + +SELECT_WIDE_ALL = "SELECT * FROM wide_test;" + +CREATE_COMPLEX_TABLE = """CREATE TABLE complex_test ( + uuid_val TEXT, + created_at TEXT, + payload TEXT +);""" + +INSERT_COMPLEX_ROW = "INSERT INTO complex_test (uuid_val, created_at, payload) VALUES (?, ?, ?);" + +SELECT_COMPLEX_ALL = "SELECT * FROM complex_test;" + +BENCHMARK_ROWS = 1_000 + +DUCKDB_BULK_TABLE = "sqlspec_bench_bulk" + +DUCKDB_BULK_CREATE = f"CREATE TABLE {DUCKDB_BULK_TABLE} (id INTEGER, payload VARCHAR)" + +DUCKDB_BULK_INSERT = f"INSERT INTO {DUCKDB_BULK_TABLE} (id, payload) VALUES (?, ?)" + +DUCKDB_BULK_COUNT = f"SELECT COUNT(*) FROM {DUCKDB_BULK_TABLE}" + +ADBC_ROWS_TABLE = "sqlspec_bench_adbc_rows" + +ADBC_ROWS_CREATE = f"CREATE TABLE IF NOT EXISTS {ADBC_ROWS_TABLE} (id INTEGER, payload VARCHAR)" + +ADBC_ROWS_TRUNCATE = f"DELETE FROM {ADBC_ROWS_TABLE}" + +ADBC_ROWS_INSERT = f"INSERT INTO {ADBC_ROWS_TABLE} (id, payload) VALUES (?, ?)" + +ADBC_ROWS_INSERT_PG = f"INSERT INTO {ADBC_ROWS_TABLE} (id, payload) VALUES ($1, $2)" + +ADBC_ROWS_SELECT = f"SELECT id, payload FROM {ADBC_ROWS_TABLE} ORDER BY id" + +MYSQL_JSON_TABLE = "sqlspec_bench_json_rows" + +MYSQL_JSON_CREATE = f"CREATE TABLE IF NOT EXISTS {MYSQL_JSON_TABLE} (id INT PRIMARY KEY, payload JSON NOT NULL)" + +MYSQL_JSON_INSERT_RAW = f"INSERT INTO {MYSQL_JSON_TABLE} (id, payload) VALUES (%s, %s)" + +MYSQL_JSON_INSERT = f"INSERT INTO {MYSQL_JSON_TABLE} (id, payload) VALUES (?, ?)" + +MYSQL_JSON_SELECT = f"SELECT id, payload FROM {MYSQL_JSON_TABLE} ORDER BY id" + +SPANNER_STRINGS_TABLE = "sqlspec_bench_strings" + +SPANNER_STRINGS_SELECT = f"SELECT id, payload FROM {SPANNER_STRINGS_TABLE} ORDER BY id" + +SCHEMA_TYPE_NUMPY_ROW_COUNT = 5_000 + +SCHEMA_TYPE_NUMPY_FIRST_FIELD_4 = 4 + +_SCHEMA_TYPE_NUMPY_PAYLOAD: list[dict[str, int]] | None = None + +_SCHEMA_TYPE_NUMPY_ROW_TYPE: type[Any] | None = None + +_SCHEMA_TYPE_NUMPY_VECTOR_TYPE: type[Any] | None = None + +ORACLE_LOB_TABLES = {"1k": "SQLSPEC_LOB_1K", "100k": "SQLSPEC_LOB_100K"} + +ORACLE_JSON_TABLE = "SQLSPEC_JSON_BENCH" + + +class BenchmarkUnavailableError(RuntimeError): + """Signal that an optional benchmark dependency or service is unavailable.""" + @click.command() @click.option( @@ -275,7 +625,6 @@ def main( ROWS_TO_INSERT = rows POOL_SIZE = pool_size - # If --profile-scenario is given, implicitly enable profiling if profile_scenario: profile = True @@ -305,7 +654,7 @@ def main( else: click.echo("No benchmark results to display.") if json_output: - _write_json_results(results, json_output, rows=rows, pool_size=pool_size, iterations=iterations) + _write_json_results(results, json_output, rows=rows, pool_size=pool_size, iterations=iterations, warmup=warmup) click.secho(f"Results written to {json_output}", fg="green") if errors: for err in errors: @@ -320,138 +669,49 @@ def main( click.echo(f"Benchmarks complete for drivers: {', '.join(driver)}") -def _new_benchmark_loop() -> asyncio.AbstractEventLoop: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - return loop +def run_benchmark( + driver: str, errors: list[str], *, iterations: int = DEFAULT_BENCH_ITERATIONS, warmup: int = DEFAULT_BENCH_WARMUP +) -> list[dict[str, Any]]: + """Run all benchmark scenarios for a driver. + Args: + driver: The database driver name (e.g., "sqlite", "asyncpg") + errors: List to append error messages to + iterations: Number of timed iterations per scenario + warmup: Number of warmup iterations (not timed) -def _close_benchmark_loop(loop: asyncio.AbstractEventLoop) -> None: - try: - loop.run_until_complete(loop.shutdown_asyncgens()) - if hasattr(loop, "shutdown_default_executor"): - loop.run_until_complete(loop.shutdown_default_executor()) - finally: - asyncio.set_event_loop(None) - loop.close() + Returns: + List of benchmark result dictionaries + """ + results: list[dict[str, Any]] = [] + for scenario in CORE_SCENARIOS: + for lib in CORE_LIBRARIES: + func = SCENARIO_REGISTRY.get((lib, driver, scenario)) + if func is None: + errors.append(f"No implementation for library={lib}, driver={driver}, scenario={scenario}") + continue -def _run_scenario_once(func: Any, loop: asyncio.AbstractEventLoop | None) -> None: - if loop is not None: - loop.run_until_complete(func()) - else: - func() + is_async = inspect.iscoroutinefunction(func) + try: + times = _run_benchmark_iterations(func, is_async=is_async, iterations=iterations, warmup=warmup) + stats = _summarize_times(times) + label = _benchmark_library_label(lib) + results.append({ + "driver": driver, + "library": label, + "library_key": lib, + "scenario": scenario, + "times": times, + **stats, + }) + except BenchmarkUnavailableError: + continue + except Exception as exc: + errors.append(f"{lib}/{driver}/{scenario}: {exc}") -def _run_benchmark_iterations( - func: Any, *, is_async: bool, iterations: int, warmup: int, profiler: cProfile.Profile | None = None -) -> list[float]: - loop = _new_benchmark_loop() if is_async else None - try: - for _ in range(warmup): - _run_scenario_once(func, loop) - - times: list[float] = [] - for _ in range(iterations): - gc.collect() - gc_was_enabled = gc.isenabled() - if gc_was_enabled: - gc.disable() - start = time.perf_counter() - try: - if profiler is not None: - profiler.enable() - _run_scenario_once(func, loop) - finally: - if profiler is not None: - profiler.disable() - if gc_was_enabled: - gc.enable() - times.append(time.perf_counter() - start) - return times - finally: - if loop is not None: - _close_benchmark_loop(loop) - - -def _summarize_times(times: list[float]) -> dict[str, float | bool]: - median_time = statistics.median(times) - stddev = statistics.stdev(times) if len(times) > 1 else 0.0 - quartiles = statistics.quantiles(times, n=4, method="inclusive") if len(times) > 1 else (0.0, 0.0, 0.0) - iqr = quartiles[2] - quartiles[0] - noise_ratio = stddev / median_time if median_time > 0 else 0.0 - return { - "time": median_time, - "stddev": stddev, - "iqr": iqr, - "noise_ratio": noise_ratio, - "noisy": noise_ratio > NOISY_STDDEV_RATIO, - } - - -def _benchmark_library_label(library: str) -> str: - """Return a display label for a benchmark library key.""" - if library == "sqlspec": - return SQLSPEC_LABEL - if library == "sqlspec_fetch_lobs_true": - return f"{SQLSPEC_LABEL} fetch_lobs=True" - if library == "sqlspec_async": - return f"{SQLSPEC_LABEL} async" - if library == "sqlspec_async_fetch_lobs_true": - return f"{SQLSPEC_LABEL} async fetch_lobs=True" - return library - - -def _driver_has_core_scenarios(driver: str) -> bool: - """Return True when a driver has any core benchmark scenario registered.""" - return any( - (library, driver, scenario) in SCENARIO_REGISTRY for scenario in CORE_SCENARIOS for library in CORE_LIBRARIES - ) - - -def run_benchmark( - driver: str, errors: list[str], *, iterations: int = DEFAULT_BENCH_ITERATIONS, warmup: int = DEFAULT_BENCH_WARMUP -) -> list[dict[str, Any]]: - """Run all benchmark scenarios for a driver. - - Args: - driver: The database driver name (e.g., "sqlite", "asyncpg") - errors: List to append error messages to - iterations: Number of timed iterations per scenario - warmup: Number of warmup iterations (not timed) - - Returns: - List of benchmark result dictionaries - """ - results: list[dict[str, Any]] = [] - - for scenario in CORE_SCENARIOS: - for lib in CORE_LIBRARIES: - func = SCENARIO_REGISTRY.get((lib, driver, scenario)) - if func is None: - errors.append(f"No implementation for library={lib}, driver={driver}, scenario={scenario}") - continue - - is_async = inspect.iscoroutinefunction(func) - - try: - times = _run_benchmark_iterations(func, is_async=is_async, iterations=iterations, warmup=warmup) - stats = _summarize_times(times) - label = _benchmark_library_label(lib) - results.append({ - "driver": driver, - "library": label, - "library_key": lib, - "scenario": scenario, - "times": times, - **stats, - }) - except BenchmarkUnavailableError: - continue - except Exception as exc: - errors.append(f"{lib}/{driver}/{scenario}: {exc}") - - return results + return results def run_benchmark_profiled( @@ -485,7 +745,6 @@ def run_benchmark_profiled( for scenario in CORE_SCENARIOS: for lib in CORE_LIBRARIES: - # If profiling a specific scenario, skip others if profile_scenario and scenario != profile_scenario: continue @@ -512,12 +771,10 @@ def run_benchmark_profiled( **summary, }) - # Save profile data prof_path = profiles_dir / f"{prof_name}.prof" profiler.dump_stats(str(prof_path)) click.echo(f" Profile saved: {prof_path}") - # Print top 20 summary console.print(f"\n [bold cyan]Profile summary: {prof_name}[/bold cyan]") profile_stats = pstats.Stats(profiler) profile_stats.strip_dirs() @@ -581,66 +838,6 @@ def run_extended_benchmark( return results -def _write_json_results( - results: list[dict[str, Any]], output_path: str, *, rows: int, pool_size: int, iterations: int -) -> None: - """Write benchmark results to a JSON file. - - Args: - results: List of benchmark result dictionaries - output_path: Path to write the JSON file - rows: Number of rows used in the benchmark - pool_size: Pool size used in the benchmark - iterations: Number of timed iterations per scenario - """ - output = { - "metadata": { - "rows": rows, - "pool_size": pool_size, - "iterations": iterations, - "mypyc_compiled": _is_compiled(), - "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z"), - }, - "results": [ - { - "driver": r["driver"], - "library": r["library"], - "library_key": r.get("library_key", r["library"]), - "scenario": r["scenario"], - "time": r["time"], - "times": r.get("times", [r["time"]]), - "stddev": r.get("stddev", 0.0), - "iqr": r.get("iqr", 0.0), - "noise_ratio": r.get("noise_ratio", 0.0), - "noisy": r.get("noisy", False), - } - for r in results - ], - } - with Path(output_path).open("w") as f: - json.dump(output, f, indent=2) - - -# --- Scenario helpers and registry --- -# SQLite implementations -# ------------------------------ - -CREATE_TEST_TABLE = "CREATE TABLE test (value TEXT);" -DROP_TEST_TABLE = "DROP TABLE IF EXISTS test;" -INSERT_TEST_VALUE = "INSERT INTO test (value) VALUES (?);" -INSERT_TEST_VALUE_ASYNCPG = "INSERT INTO test (value) VALUES ($1);" -SELECT_TEST_VALUES = "SELECT * FROM test;" -INSERT_TEST_VALUE_SQLA = "INSERT INTO test (value) VALUES (:value);" - - -def _optimize_raw_sqlite(conn: sqlite3.Connection) -> None: - """Apply same PRAGMAs that sqlspec applies for fair comparison.""" - conn.execute("PRAGMA journal_mode = WAL") - conn.execute("PRAGMA synchronous = NORMAL") - conn.execute("PRAGMA busy_timeout = 5000") - conn.execute("PRAGMA foreign_keys = ON") - - def raw_sqlite_initialization() -> None: with tempfile.NamedTemporaryFile(suffix=".db") as tmp: conn = sqlite3.connect(tmp.name) @@ -654,7 +851,6 @@ def raw_sqlite_write_heavy() -> None: conn = sqlite3.connect(tmp.name) _optimize_raw_sqlite(conn) conn.execute(CREATE_TEST_TABLE) - # Use executemany for fair comparison data = [(f"value_{i}",) for i in range(ROWS_TO_INSERT)] conn.executemany(INSERT_TEST_VALUE, data) conn.commit() @@ -689,7 +885,6 @@ def sqlspec_sqlite_write_heavy() -> None: config = SqliteConfig(connection_config={"database": tmp.name}) with spec.provide_session(config) as session: session.execute(CREATE_TEST_TABLE) - # Use execute_many for bulk inserts data: Sequence[tuple[str]] = [(f"value_{i}",) for i in range(ROWS_TO_INSERT)] session.execute_many(INSERT_TEST_VALUE, data) @@ -706,16 +901,6 @@ def sqlspec_sqlite_read_heavy() -> None: assert len(rows) == ROWS_TO_INSERT -def _get_sqlalchemy() -> tuple[Any, Any]: - """Import SQLAlchemy lazily.""" - try: - from sqlalchemy import create_engine, text - except ImportError: - return None, None - else: - return create_engine, text - - def sqlalchemy_sqlite_initialization() -> None: create_engine, text = _get_sqlalchemy() if create_engine is None: @@ -735,7 +920,6 @@ def sqlalchemy_sqlite_write_heavy() -> None: engine = create_engine(f"sqlite:///{tmp.name}") with engine.connect() as conn: conn.execute(text(CREATE_TEST_TABLE)) - # Use insert with bindparams for fair bulk comparison data = [{"value": f"value_{i}"} for i in range(ROWS_TO_INSERT)] conn.execute(text(INSERT_TEST_VALUE_SQLA), data) conn.commit() @@ -757,29 +941,13 @@ def sqlalchemy_sqlite_read_heavy() -> None: assert len(rows) == ROWS_TO_INSERT -# DuckDB implementations -# DuckDB is sync like sqlite, but uses its own driver -# ------------------------------ - - -def _get_duckdb() -> Any: - """Import duckdb lazily.""" - try: - import duckdb - except ImportError: - return None - else: - return duckdb - - def raw_duckdb_initialization() -> None: duckdb = _get_duckdb() if duckdb is None: return - # DuckDB needs to create the file itself - use temp name then delete with tempfile.NamedTemporaryFile(suffix=".duckdb", delete=False) as tmp: tmp_path = Path(tmp.name) - tmp_path.unlink() # Delete so DuckDB can create fresh + tmp_path.unlink() try: conn = duckdb.connect(str(tmp_path)) conn.execute(CREATE_TEST_TABLE) @@ -993,72 +1161,17 @@ def sqlspec_duckdb_bulk() -> None: tmp_path.unlink() -def _get_adbc_dbapi() -> Any: - """Import the ADBC DB-API lazily.""" - try: - from adbc_driver_manager import dbapi - except ImportError: - return None - else: - return dbapi - - -def _get_adbc_config() -> Any: - """Import AdbcConfig lazily.""" - try: - from sqlspec.adapters.adbc import AdbcConfig - except ImportError: - return None - else: - return AdbcConfig - - -def _adbc_benchmark_connection_config() -> dict[str, Any] | None: - """Build an ADBC benchmark connection from service-provided environment values.""" - uri = os.environ.get("SQLSPEC_BENCH_ADBC_URI") - driver_name = os.environ.get("SQLSPEC_BENCH_ADBC_DRIVER_NAME") - if not uri or not driver_name: - return None - config: dict[str, Any] = {"uri": uri, "driver_name": driver_name} - for env_name, config_name in ( - ("SQLSPEC_BENCH_ADBC_USERNAME", "username"), - ("SQLSPEC_BENCH_ADBC_PASSWORD", "password"), - ("SQLSPEC_BENCH_ADBC_GIZMOSQL_BACKEND", "gizmosql_backend"), - ): - value = os.environ.get(env_name) - if value: - config[config_name] = value - if os.environ.get("SQLSPEC_BENCH_ADBC_TLS_SKIP_VERIFY") == "1": - config["tls_skip_verify"] = True - return config - - -def _adbc_benchmark_rows() -> list[tuple[int, str]]: - """Return the stable row payload used by the ADBC benchmark.""" - return [(index, f"value_{index}") for index in range(BENCHMARK_ROWS)] - - -def _connect_adbc(dbapi: Any, config: dict[str, Any]) -> Any: - driver_name = config["driver_name"] - if driver_name in {"adbc_driver_sqlite", "adbc_driver_duckdb"}: - module = importlib.import_module(f"{driver_name}.dbapi") - return module.connect(uri=config["uri"], autocommit=True) - db_kwargs = { - key: config[key] for key in ("username", "password", "gizmosql_backend", "tls_skip_verify") if key in config - } - return dbapi.connect(driver=driver_name, uri=config["uri"], db_kwargs=db_kwargs, autocommit=True) - - def raw_adbc_rows() -> None: """Fetch rows through the native ADBC DB-API path.""" dbapi = _get_adbc_dbapi() config = _adbc_benchmark_connection_config() if dbapi is None or config is None: _benchmark_unavailable() + insert_sql = ADBC_ROWS_INSERT_PG if "postgres" in str(config.get("driver_name", "")) else ADBC_ROWS_INSERT with _connect_adbc(dbapi, config) as connection, connection.cursor() as cursor: cursor.execute(ADBC_ROWS_CREATE) cursor.execute(ADBC_ROWS_TRUNCATE) - cursor.executemany(ADBC_ROWS_INSERT, _adbc_benchmark_rows()) + cursor.executemany(insert_sql, _adbc_benchmark_rows()) cursor.execute(ADBC_ROWS_SELECT) rows = cursor.fetchall() assert len(rows) == BENCHMARK_ROWS @@ -1082,60 +1195,13 @@ def sqlspec_adbc_rows() -> None: config.close_pool() -def _get_mysql_connector() -> Any: - """Import mysql-connector lazily.""" - try: - import mysql.connector - except ImportError: - return None - else: - return mysql.connector - - -def _get_mysql_connector_config() -> Any: - """Import MysqlConnectorSyncConfig lazily.""" - try: - from sqlspec.adapters.mysqlconnector import MysqlConnectorSyncConfig - except ImportError: - return None - else: - return MysqlConnectorSyncConfig - - -def _mysql_benchmark_connection_config() -> dict[str, Any] | None: - """Build a MySQL benchmark connection from service-provided environment values.""" - required: dict[str, Any] = { - "host": os.environ.get("SQLSPEC_BENCH_MYSQL_HOST"), - "port": os.environ.get("SQLSPEC_BENCH_MYSQL_PORT"), - "user": os.environ.get("SQLSPEC_BENCH_MYSQL_USER"), - "password": os.environ.get("SQLSPEC_BENCH_MYSQL_PASSWORD"), - "database": os.environ.get("SQLSPEC_BENCH_MYSQL_DATABASE"), - } - if any(value is None for value in required.values()): - return None - required["port"] = int(required["port"]) - required["use_pure"] = True - required["autocommit"] = True - return required - - -def _json_benchmark_rows() -> list[tuple[int, str]]: - """Return the stable JSON payload used by the MySQL benchmark.""" - return [(index, json.dumps({"index": index, "values": [index, index + 1]})) for index in range(BENCHMARK_ROWS)] - - -def _json_row_payload(row: Any) -> Any: - payload = row.get("payload") if isinstance(row, dict) else row[1] - return json.loads(payload) if isinstance(payload, str) else payload - - -def raw_mysqlconnector_json_rows() -> None: - """Fetch JSON rows through the native MySQL connector.""" - connector = _get_mysql_connector() - connection_config = _mysql_benchmark_connection_config() - if connector is None or connection_config is None: - _benchmark_unavailable() - connection = connector.connect(**connection_config) +def raw_mysqlconnector_json_rows() -> None: + """Fetch JSON rows through the native MySQL connector.""" + connector = _get_mysql_connector() + connection_config = _mysql_benchmark_connection_config() + if connector is None or connection_config is None: + _benchmark_unavailable() + connection = connector.connect(**connection_config) try: with connection.cursor() as cursor: cursor.execute(MYSQL_JSON_CREATE) @@ -1169,19 +1235,6 @@ def sqlspec_mysqlconnector_json_rows() -> None: assert _json_row_payload(rows[0])["values"] == [0, 1] -def _spanner_benchmark_config() -> dict[str, Any] | None: - """Build a Spanner benchmark connection from service-provided environment values.""" - required: dict[str, Any] = { - "project": os.environ.get("SQLSPEC_BENCH_SPANNER_PROJECT"), - "instance_id": os.environ.get("SQLSPEC_BENCH_SPANNER_INSTANCE_ID"), - "database_id": os.environ.get("SQLSPEC_BENCH_SPANNER_DATABASE_ID"), - "api_endpoint": os.environ.get("SQLSPEC_BENCH_SPANNER_API_ENDPOINT"), - } - if any(value is None for value in required.values()): - return None - return required - - def raw_spanner_strings() -> None: """Fetch string rows through the native Google Cloud Spanner client.""" try: @@ -1232,17 +1285,6 @@ def sqlspec_spanner_strings() -> None: assert len(rows) == BENCHMARK_ROWS -def _get_duckdb_engine() -> tuple[Any, Any]: - """Import SQLAlchemy with duckdb_engine lazily.""" - try: - importlib.import_module("duckdb_engine") - from sqlalchemy import create_engine, text - except ImportError: - return None, None - else: - return create_engine, text - - def sqlalchemy_duckdb_initialization() -> None: create_engine, text = _get_duckdb_engine() if create_engine is None: @@ -1344,11 +1386,6 @@ def sqlalchemy_duckdb_repeated_queries() -> None: tmp_path.unlink() -# Iterative insert scenarios - tests per-call overhead -# This is what euri10's original benchmark measured for sqlspec -# but not for raw/sqlalchemy (which used executemany) - - def raw_sqlite_iterative_inserts() -> None: """Individual inserts in a loop - shows per-call overhead.""" with tempfile.NamedTemporaryFile(suffix=".db") as tmp: @@ -1386,13 +1423,6 @@ def sqlalchemy_sqlite_iterative_inserts() -> None: conn.commit() -# Query cache scenarios - tests repeated single-row operations -# These stress the query preparation/caching path -SELECT_BY_VALUE = "SELECT * FROM test WHERE value = ?;" -SELECT_BY_VALUE_ASYNCPG = "SELECT * FROM test WHERE value = $1;" -SELECT_BY_VALUE_SQLA = "SELECT * FROM test WHERE value = :value;" - - def raw_sqlite_repeated_queries() -> None: """Repeated single-row queries - tests query preparation overhead.""" with tempfile.NamedTemporaryFile(suffix=".db") as tmp: @@ -1402,7 +1432,6 @@ def raw_sqlite_repeated_queries() -> None: data = [(f"value_{i}",) for i in range(ROWS_TO_INSERT)] conn.executemany(INSERT_TEST_VALUE, data) conn.commit() - # Query same rows repeatedly with different params for i in range(ROWS_TO_INSERT): cursor = conn.execute(SELECT_BY_VALUE, (f"value_{i % 100}",)) cursor.fetchone() @@ -1418,8 +1447,6 @@ def sqlspec_sqlite_repeated_queries() -> None: session.execute(CREATE_TEST_TABLE) data: Sequence[tuple[str]] = [(f"value_{i}",) for i in range(ROWS_TO_INSERT)] session.execute_many(INSERT_TEST_VALUE, data) - # Query same rows repeatedly with different params - # This should hit the query cache after the first few iterations for i in range(ROWS_TO_INSERT): session.fetch_one_or_none(SELECT_BY_VALUE, (f"value_{i % 100}",)) @@ -1436,53 +1463,11 @@ def sqlalchemy_sqlite_repeated_queries() -> None: data = [{"value": f"value_{i}"} for i in range(ROWS_TO_INSERT)] conn.execute(text(INSERT_TEST_VALUE_SQLA), data) conn.commit() - # Query same rows repeatedly with different params for i in range(ROWS_TO_INSERT): result = conn.execute(text(SELECT_BY_VALUE_SQLA), {"value": f"value_{i % 100}"}) result.fetchone() -# Aiosqlite implementations -# These test async sqlite performance - - -def _check_pool_leak(pool: Any, scenario_name: str) -> None: - """Check for connection leaks in a pool. - - Args: - pool: Connection pool with size() and checked_out() methods - scenario_name: Name of the scenario for error reporting - """ - if pool is None: - return - - with suppress(AttributeError, TypeError): - total = pool.size() - checked_out = pool.checked_out() - if checked_out > 0: - _leaked_pools.append(f"{scenario_name}: {checked_out}/{total} connections leaked") - - -def _get_aiosqlite() -> Any: - """Import aiosqlite lazily.""" - try: - import aiosqlite - except ImportError: - return None - else: - return aiosqlite - - -def _get_aiosqlite_config() -> Any: - """Import AiosqliteConfig lazily.""" - try: - from sqlspec.adapters.aiosqlite import AiosqliteConfig - except ImportError: - return None - else: - return AiosqliteConfig - - async def raw_aiosqlite_initialization() -> None: aiosqlite = _get_aiosqlite() if aiosqlite is None: @@ -1576,7 +1561,6 @@ async def sqlspec_aiosqlite_initialization() -> None: aiosqlite_config_type = _get_aiosqlite_config() if aiosqlite_config_type is None: return - # Use delete=False so we control when the file is deleted (after pool close) with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: tmp_path = Path(tmp.name) try: @@ -1585,7 +1569,6 @@ async def sqlspec_aiosqlite_initialization() -> None: async with spec.provide_session(config) as session: await session.execute(DROP_TEST_TABLE) await session.execute(CREATE_TEST_TABLE) - # Properly close the pool to release all connections _check_pool_leak(config.connection_instance, "aiosqlite/initialization") await config.close_pool() finally: @@ -1809,67 +1792,6 @@ async def sqlalchemy_aiosqlite_repeated_queries() -> None: await anyio.Path(tmp_path).unlink() -# Asyncpg implementations -# These require asyncpg and optionally SQLAlchemy[asyncio] to be installed - - -def _get_asyncpg() -> Any: - """Import asyncpg lazily.""" - try: - from asyncpg import connect - except ImportError: - return None - else: - return connect - - -def _get_asyncpg_config() -> Any: - """Import AsyncpgConfig lazily.""" - try: - from sqlspec.adapters.asyncpg import AsyncpgConfig - except ImportError: - return None - else: - return AsyncpgConfig - - -def _get_async_sqlalchemy() -> tuple[Any, Any]: - """Import async SQLAlchemy lazily.""" - try: - from sqlalchemy import text - from sqlalchemy.ext.asyncio import create_async_engine - except ImportError: - return None, None - else: - return create_async_engine, text - - -def _postgres_benchmark_dsn() -> str: - dsn = os.environ.get(POSTGRES_DSN_ENV) - if not dsn: - _benchmark_unavailable() - return dsn - - -def _cockroach_benchmark_dsn() -> str: - dsn = os.environ.get(COCKROACH_DSN_ENV) - if not dsn: - _benchmark_unavailable() - return dsn - - -def _asyncpg_connect_kwargs(dsn: str) -> dict[str, Any]: - """Adapt a service DSN to asyncpg's keyword set.""" - parsed = urlsplit(dsn) - query = dict(parse_qsl(parsed.query, keep_blank_values=True)) - sslmode = query.pop("sslmode", None) - clean_dsn = urlunsplit((parsed.scheme, parsed.netloc, parsed.path, urlencode(query), parsed.fragment)) - kwargs: dict[str, Any] = {"dsn": clean_dsn} - if sslmode == "disable": - kwargs["ssl"] = False - return kwargs - - async def raw_asyncpg_initialization() -> None: connect = _get_asyncpg() if connect is None: @@ -1887,7 +1809,6 @@ async def raw_asyncpg_write_heavy() -> None: conn = await connect(**_asyncpg_connect_kwargs(_postgres_benchmark_dsn())) await conn.execute(DROP_TEST_TABLE) await conn.execute(CREATE_TEST_TABLE) - # Use executemany for fair comparison data = [(f"value_{i}",) for i in range(ROWS_TO_INSERT)] await conn.executemany(INSERT_TEST_VALUE_ASYNCPG, data) await conn.close() @@ -2082,180 +2003,36 @@ async def sqlalchemy_asyncpg_repeated_queries() -> None: await engine.dispose() -SERVICE_ROWS_CREATE = "CREATE TABLE IF NOT EXISTS sqlspec_bench_rows (id INTEGER PRIMARY KEY, payload TEXT)" -SERVICE_ROWS_DROP = "DROP TABLE IF EXISTS sqlspec_bench_rows" -SERVICE_ROWS_INSERT_PG = "INSERT INTO sqlspec_bench_rows (id, payload) VALUES (%s, %s)" -SERVICE_ROWS_INSERT_ASYNCPG = "INSERT INTO sqlspec_bench_rows (id, payload) VALUES ($1, $2)" -SERVICE_ROWS_SELECT = "SELECT id, payload FROM sqlspec_bench_rows ORDER BY id" +def raw_psycopg_sync_rows() -> None: + _run_raw_psycopg_sync_rows(_postgres_benchmark_dsn(), cockroach=False) -def _service_benchmark_rows() -> list[tuple[int, str]]: - return [(index, f"value_{index}") for index in range(BENCHMARK_ROWS)] +def sqlspec_psycopg_sync_rows() -> None: + _run_sqlspec_sync_service_rows(_postgres_benchmark_dsn(), driver="psycopg_sync") -def _get_psycopg() -> Any: - try: - import psycopg - except ImportError: - return None - else: - return psycopg +async def raw_psycopg_async_rows() -> None: + await _run_raw_psycopg_async_rows(_postgres_benchmark_dsn()) -def _run_raw_psycopg_sync_rows(dsn: str, *, cockroach: bool) -> None: - psycopg = _get_psycopg() - if psycopg is None: - _benchmark_unavailable() - if cockroach: - from psycopg import crdb +async def sqlspec_psycopg_async_rows() -> None: + await _run_sqlspec_async_service_rows(_postgres_benchmark_dsn(), driver="psycopg_async") - connect = crdb.connect - else: - connect = psycopg.connect - with connect(dsn) as connection: - with connection.cursor() as cursor: - cursor.execute(SERVICE_ROWS_DROP) - cursor.execute(SERVICE_ROWS_CREATE) - cursor.executemany(SERVICE_ROWS_INSERT_PG, _service_benchmark_rows()) - cursor.execute(SERVICE_ROWS_SELECT) - rows = cursor.fetchall() - connection.commit() - assert len(rows) == BENCHMARK_ROWS +async def raw_asyncpg_rows() -> None: + await _run_raw_asyncpg_rows(_postgres_benchmark_dsn()) -def _get_sync_service_config(driver: str) -> Any: - try: - if driver == "psycopg_sync": - from sqlspec.adapters.psycopg import PsycopgSyncConfig - return PsycopgSyncConfig - from sqlspec.adapters.cockroach_psycopg import CockroachPsycopgSyncConfig - except ImportError: - return None - else: - return CockroachPsycopgSyncConfig +async def sqlspec_asyncpg_rows() -> None: + await _run_sqlspec_async_service_rows(_postgres_benchmark_dsn(), driver="asyncpg") -def _run_sqlspec_sync_service_rows(dsn: str, *, driver: str) -> None: - config_class = _get_sync_service_config(driver) - if config_class is None: - _benchmark_unavailable() - config = config_class(connection_config={"conninfo": dsn, "min_size": 1, "max_size": max(1, POOL_SIZE)}) - try: - spec = SQLSpec() - with spec.provide_session(config) as session: - session.execute(SERVICE_ROWS_DROP) - session.execute(SERVICE_ROWS_CREATE) - session.execute_many(SERVICE_ROWS_INSERT_PG, _service_benchmark_rows()) - rows = session.fetch(SERVICE_ROWS_SELECT) - assert len(rows) == BENCHMARK_ROWS - finally: - config.close_pool() +def raw_cockroach_psycopg_sync_rows() -> None: + _run_raw_psycopg_sync_rows(_cockroach_benchmark_dsn(), cockroach=True) -async def _run_raw_psycopg_async_rows(dsn: str) -> None: - psycopg = _get_psycopg() - if psycopg is None: - _benchmark_unavailable() - connection = await psycopg.AsyncConnection.connect(dsn) - try: - async with connection.cursor() as cursor: - await cursor.execute(SERVICE_ROWS_DROP) - await cursor.execute(SERVICE_ROWS_CREATE) - await cursor.executemany(SERVICE_ROWS_INSERT_PG, _service_benchmark_rows()) - await cursor.execute(SERVICE_ROWS_SELECT) - rows = await cursor.fetchall() - await connection.commit() - finally: - await connection.close() - assert len(rows) == BENCHMARK_ROWS - - -async def _run_raw_asyncpg_rows(dsn: str) -> None: - connect = _get_asyncpg() - if connect is None: - _benchmark_unavailable() - connection = await connect(**_asyncpg_connect_kwargs(dsn)) - try: - await connection.execute(SERVICE_ROWS_DROP) - await connection.execute(SERVICE_ROWS_CREATE) - await connection.executemany(SERVICE_ROWS_INSERT_ASYNCPG, _service_benchmark_rows()) - rows = await connection.fetch(SERVICE_ROWS_SELECT) - finally: - await connection.close() - assert len(rows) == BENCHMARK_ROWS - - -def _get_async_service_config(driver: str) -> Any: - try: - if driver in {"psycopg_async", "cockroach_psycopg_async"}: - if driver == "psycopg_async": - from sqlspec.adapters.psycopg import PsycopgAsyncConfig - - return PsycopgAsyncConfig - from sqlspec.adapters.cockroach_psycopg import CockroachPsycopgAsyncConfig - - return CockroachPsycopgAsyncConfig - if driver == "asyncpg": - from sqlspec.adapters.asyncpg import AsyncpgConfig - - return AsyncpgConfig - from sqlspec.adapters.cockroach_asyncpg import CockroachAsyncpgConfig - except ImportError: - return None - else: - return CockroachAsyncpgConfig - - -async def _run_sqlspec_async_service_rows(dsn: str, *, driver: str) -> None: - config_class = _get_async_service_config(driver) - if config_class is None: - _benchmark_unavailable() - connection_key = "dsn" if "asyncpg" in driver else "conninfo" - config = config_class(connection_config={connection_key: dsn, "min_size": 1, "max_size": max(1, POOL_SIZE)}) - try: - spec = SQLSpec() - async with spec.provide_session(config) as session: - await session.execute(SERVICE_ROWS_DROP) - await session.execute(SERVICE_ROWS_CREATE) - insert_sql = SERVICE_ROWS_INSERT_ASYNCPG if "asyncpg" in driver else SERVICE_ROWS_INSERT_PG - await session.execute_many(insert_sql, _service_benchmark_rows()) - rows = await session.fetch(SERVICE_ROWS_SELECT) - assert len(rows) == BENCHMARK_ROWS - finally: - await config.close_pool() - - -def raw_psycopg_sync_rows() -> None: - _run_raw_psycopg_sync_rows(_postgres_benchmark_dsn(), cockroach=False) - - -def sqlspec_psycopg_sync_rows() -> None: - _run_sqlspec_sync_service_rows(_postgres_benchmark_dsn(), driver="psycopg_sync") - - -async def raw_psycopg_async_rows() -> None: - await _run_raw_psycopg_async_rows(_postgres_benchmark_dsn()) - - -async def sqlspec_psycopg_async_rows() -> None: - await _run_sqlspec_async_service_rows(_postgres_benchmark_dsn(), driver="psycopg_async") - - -async def raw_asyncpg_rows() -> None: - await _run_raw_asyncpg_rows(_postgres_benchmark_dsn()) - - -async def sqlspec_asyncpg_rows() -> None: - await _run_sqlspec_async_service_rows(_postgres_benchmark_dsn(), driver="asyncpg") - - -def raw_cockroach_psycopg_sync_rows() -> None: - _run_raw_psycopg_sync_rows(_cockroach_benchmark_dsn(), cockroach=True) - - -def sqlspec_cockroach_psycopg_sync_rows() -> None: - _run_sqlspec_sync_service_rows(_cockroach_benchmark_dsn(), driver="cockroach_psycopg_sync") +def sqlspec_cockroach_psycopg_sync_rows() -> None: + _run_sqlspec_sync_service_rows(_cockroach_benchmark_dsn(), driver="cockroach_psycopg_sync") async def raw_cockroach_psycopg_async_rows() -> None: @@ -2274,65 +2051,6 @@ async def sqlspec_cockroach_asyncpg_rows() -> None: await _run_sqlspec_async_service_rows(_cockroach_benchmark_dsn(), driver="cockroach_asyncpg") -# =========================================================================== -# Extended benchmark scenarios -# These exercise sqlspec-specific features (dict key transforms, schema -# mapping, complex parameter types, thin path stress testing). -# =========================================================================== - -# --- SQL constants for extended scenarios --- - -CREATE_WIDE_TABLE = """CREATE TABLE wide_test ( - first_name TEXT, - last_name TEXT, - email_address TEXT, - phone_number TEXT, - street_address TEXT, - postal_code TEXT, - country_name TEXT, - birth_date TEXT, - account_balance TEXT, - is_active TEXT -);""" - -INSERT_WIDE_ROW = ( - "INSERT INTO wide_test " - "(first_name, last_name, email_address, phone_number, street_address, " - "postal_code, country_name, birth_date, account_balance, is_active) " - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);" -) - -SELECT_WIDE_ALL = "SELECT * FROM wide_test;" - -CREATE_COMPLEX_TABLE = """CREATE TABLE complex_test ( - uuid_val TEXT, - created_at TEXT, - payload TEXT -);""" - -INSERT_COMPLEX_ROW = "INSERT INTO complex_test (uuid_val, created_at, payload) VALUES (?, ?, ?);" - -SELECT_COMPLEX_ALL = "SELECT * FROM complex_test;" - -BENCHMARK_ROWS = 1_000 -DUCKDB_BULK_TABLE = "sqlspec_bench_bulk" -DUCKDB_BULK_CREATE = f"CREATE TABLE {DUCKDB_BULK_TABLE} (id INTEGER, payload VARCHAR)" -DUCKDB_BULK_INSERT = f"INSERT INTO {DUCKDB_BULK_TABLE} (id, payload) VALUES (?, ?)" -DUCKDB_BULK_COUNT = f"SELECT COUNT(*) FROM {DUCKDB_BULK_TABLE}" -ADBC_ROWS_TABLE = "sqlspec_bench_adbc_rows" -ADBC_ROWS_CREATE = f"CREATE TABLE IF NOT EXISTS {ADBC_ROWS_TABLE} (id INTEGER, payload VARCHAR)" -ADBC_ROWS_TRUNCATE = f"DELETE FROM {ADBC_ROWS_TABLE}" -ADBC_ROWS_INSERT = f"INSERT INTO {ADBC_ROWS_TABLE} (id, payload) VALUES (?, ?)" -ADBC_ROWS_SELECT = f"SELECT id, payload FROM {ADBC_ROWS_TABLE} ORDER BY id" -MYSQL_JSON_TABLE = "sqlspec_bench_json_rows" -MYSQL_JSON_CREATE = f"CREATE TABLE IF NOT EXISTS {MYSQL_JSON_TABLE} (id INT PRIMARY KEY, payload JSON NOT NULL)" -MYSQL_JSON_INSERT_RAW = f"INSERT INTO {MYSQL_JSON_TABLE} (id, payload) VALUES (%s, %s)" -MYSQL_JSON_INSERT = f"INSERT INTO {MYSQL_JSON_TABLE} (id, payload) VALUES (?, ?)" -MYSQL_JSON_SELECT = f"SELECT id, payload FROM {MYSQL_JSON_TABLE} ORDER BY id" -SPANNER_STRINGS_TABLE = "sqlspec_bench_strings" -SPANNER_STRINGS_SELECT = f"SELECT id, payload FROM {SPANNER_STRINGS_TABLE} ORDER BY id" - - class WideRowDict(TypedDict): """TypedDict schema matching the wide_test table for schema_mapping benchmark.""" @@ -2348,54 +2066,6 @@ class WideRowDict(TypedDict): is_active: str -SCHEMA_TYPE_NUMPY_ROW_COUNT = 5_000 -SCHEMA_TYPE_NUMPY_FIRST_FIELD_4 = 4 -_SCHEMA_TYPE_NUMPY_PAYLOAD: list[dict[str, int]] | None = None -_SCHEMA_TYPE_NUMPY_ROW_TYPE: type[Any] | None = None -_SCHEMA_TYPE_NUMPY_VECTOR_TYPE: type[Any] | None = None - - -def _schema_type_numpy_payload() -> list[dict[str, int]]: - """Return the stable 5000x5 payload used by the schema_type numpy benchmark.""" - global _SCHEMA_TYPE_NUMPY_PAYLOAD - if _SCHEMA_TYPE_NUMPY_PAYLOAD is None: - _SCHEMA_TYPE_NUMPY_PAYLOAD = [ - {"field_0": index, "field_1": index + 1, "field_2": index + 2, "field_3": index + 3, "field_4": index + 4} - for index in range(SCHEMA_TYPE_NUMPY_ROW_COUNT) - ] - return _SCHEMA_TYPE_NUMPY_PAYLOAD - - -def _schema_type_numpy_row_type() -> type[Any]: - """Return the cached msgspec row type used by the schema_type numpy benchmark.""" - global _SCHEMA_TYPE_NUMPY_ROW_TYPE - if _SCHEMA_TYPE_NUMPY_ROW_TYPE is None: - import msgspec - - class SchemaTypeNumpyRow(msgspec.Struct): - field_0: int - field_1: int - field_2: int - field_3: int - field_4: int - - _SCHEMA_TYPE_NUMPY_ROW_TYPE = SchemaTypeNumpyRow - return _SCHEMA_TYPE_NUMPY_ROW_TYPE - - -def _schema_type_numpy_vector_type() -> type[Any]: - """Return the cached msgspec vector row type used to assert ndarray fallback behavior.""" - global _SCHEMA_TYPE_NUMPY_VECTOR_TYPE - if _SCHEMA_TYPE_NUMPY_VECTOR_TYPE is None: - import msgspec - - class SchemaTypeNumpyVectorRow(msgspec.Struct): - values: list[float] - - _SCHEMA_TYPE_NUMPY_VECTOR_TYPE = SchemaTypeNumpyVectorRow - return _SCHEMA_TYPE_NUMPY_VECTOR_TYPE - - def assert_schema_type_numpy_vector_fallback() -> None: """Assert ndarray -> list[float] fallback still works for msgspec schema conversion.""" import numpy as np @@ -2406,35 +2076,6 @@ def assert_schema_type_numpy_vector_fallback() -> None: assert converted[0].values == [1.0, 2.0] -def _generate_wide_row(i: int) -> tuple[str, ...]: - """Generate a single row tuple for the wide_test table.""" - return ( - f"First{i}", - f"Last{i}", - f"user{i}@example.com", - f"+1-555-{i:04d}", - f"{i} Main Street", - f"{10000 + i}", - "United States", - f"199{i % 10}-{1 + (i % 12):02d}-{1 + (i % 28):02d}", - f"{100.0 + i * 0.5:.2f}", - "true" if i % 2 == 0 else "false", - ) - - -def _generate_complex_row(i: int) -> tuple[str, str, str]: - """Generate a single row tuple for the complex_test table.""" - import uuid as uuid_mod - - uid = str(uuid_mod.UUID(int=i)) - created_at = f"2024-{1 + (i % 12):02d}-{1 + (i % 28):02d}T{i % 24:02d}:{i % 60:02d}:00" - payload = f'{{"key": "value_{i}", "count": {i}, "tags": ["a", "b"]}}' - return (uid, created_at, payload) - - -# --- dict_key_transform scenarios --- - - def raw_sqlite_dict_key_transform() -> None: """SELECT 10K rows with 10 columns, then transform keys to camelCase (raw).""" with tempfile.NamedTemporaryFile(suffix=".db") as tmp: @@ -2444,12 +2085,10 @@ def raw_sqlite_dict_key_transform() -> None: data = [_generate_wide_row(i) for i in range(ROWS_TO_INSERT)] conn.executemany(INSERT_WIDE_ROW, data) conn.commit() - # Raw driver: fetch as tuples; build dicts manually then transform conn.row_factory = sqlite3.Row cursor = conn.execute(SELECT_WIDE_ALL) rows = cursor.fetchall() assert len(rows) == ROWS_TO_INSERT - # No transform for raw - just fetch (raw baseline) conn.close() @@ -2464,17 +2103,12 @@ def sqlspec_sqlite_dict_key_transform() -> None: session.execute_many(INSERT_WIDE_ROW, data) rows = session.fetch(SELECT_WIDE_ALL) assert len(rows) == ROWS_TO_INSERT - # Apply dict key transformation from snake_case to camelCase transformed = [transform_dict_keys(row, camelize) for row in rows] assert len(transformed) == ROWS_TO_INSERT - # Verify transformation worked on first row first = transformed[0] assert "firstName" in first -# --- schema_mapping scenarios --- - - def raw_sqlite_schema_mapping() -> None: """SELECT 10K rows and return as raw tuples (baseline for schema mapping).""" with tempfile.NamedTemporaryFile(suffix=".db") as tmp: @@ -2501,14 +2135,10 @@ def sqlspec_sqlite_schema_mapping() -> None: session.execute_many(INSERT_WIDE_ROW, data) rows = session.fetch(SELECT_WIDE_ALL, schema_type=WideRowDict) assert len(rows) == ROWS_TO_INSERT - # Verify schema mapping worked first = rows[0] assert "first_name" in first -# --- schema_type_numpy scenarios --- - - def raw_sqlite_schema_type_numpy() -> None: """Legacy baseline: pre-walk a 5000x5 payload before msgspec schema conversion.""" row_type = _schema_type_numpy_row_type() @@ -2525,9 +2155,6 @@ def sqlspec_sqlite_schema_type_numpy() -> None: assert rows[0].field_4 == SCHEMA_TYPE_NUMPY_FIRST_FIELD_4 -# --- complex_parameters scenarios --- - - def raw_sqlite_complex_parameters() -> None: """INSERT rows with complex parameter types: UUID, datetime, JSON (raw).""" with tempfile.NamedTemporaryFile(suffix=".db") as tmp: @@ -2537,7 +2164,6 @@ def raw_sqlite_complex_parameters() -> None: data = [_generate_complex_row(i) for i in range(ROWS_TO_INSERT)] conn.executemany(INSERT_COMPLEX_ROW, data) conn.commit() - # Read back to verify cursor = conn.execute(SELECT_COMPLEX_ALL) rows = cursor.fetchall() assert len(rows) == ROWS_TO_INSERT @@ -2553,43 +2179,1516 @@ def sqlspec_sqlite_complex_parameters() -> None: session.execute(CREATE_COMPLEX_TABLE) data: Sequence[tuple[str, str, str]] = [_generate_complex_row(i) for i in range(ROWS_TO_INSERT)] session.execute_many(INSERT_COMPLEX_ROW, data) - # Read back to verify rows = session.fetch(SELECT_COMPLEX_ALL) assert len(rows) == ROWS_TO_INSERT -# --- thin_path_stress scenarios --- +def raw_sqlite_thin_path_stress() -> None: + """100K individual INSERTs to stress-test per-call execution path (raw).""" + stress_count = ROWS_TO_INSERT * 10 + with tempfile.NamedTemporaryFile(suffix=".db") as tmp: + conn = sqlite3.connect(tmp.name) + _optimize_raw_sqlite(conn) + conn.execute(CREATE_TEST_TABLE) + for i in range(stress_count): + conn.execute(INSERT_TEST_VALUE, (f"value_{i}",)) + conn.commit() + conn.close() + + +def sqlspec_sqlite_thin_path_stress() -> None: + """100K individual INSERTs to stress-test the thin execute path (sqlspec).""" + stress_count = ROWS_TO_INSERT * 10 + with tempfile.NamedTemporaryFile(suffix=".db") as tmp: + spec = SQLSpec() + config = SqliteConfig(connection_config={"database": tmp.name}) + with spec.provide_session(config) as session: + session.execute(CREATE_TEST_TABLE) + for i in range(stress_count): + session.execute(INSERT_TEST_VALUE, (f"value_{i}",)) + + +def raw_oracle_lob_fetch_1k() -> None: + """Fetch 100 1 KiB Oracle CLOB rows through raw python-oracledb.""" + _run_raw_oracle_lob_fetch("1k") + + +def raw_oracle_lob_fetch_100k() -> None: + """Fetch 100 100 KiB Oracle CLOB rows through raw python-oracledb.""" + _run_raw_oracle_lob_fetch("100k") + + +def sqlspec_oracle_lob_fetch_1k() -> None: + """Fetch 100 1 KiB Oracle CLOB rows with sqlspec direct LOB fetching.""" + _run_sqlspec_oracle_lob_fetch("1k", fetch_lobs=False) + + +def sqlspec_oracle_lob_fetch_100k() -> None: + """Fetch 100 100 KiB Oracle CLOB rows with sqlspec direct LOB fetching.""" + _run_sqlspec_oracle_lob_fetch("100k", fetch_lobs=False) + + +def sqlspec_oracle_lob_fetch_fetch_lobs_true_1k() -> None: + """Fetch 100 1 KiB Oracle CLOB rows with sqlspec LOB locators enabled.""" + _run_sqlspec_oracle_lob_fetch("1k", fetch_lobs=True) + + +def sqlspec_oracle_lob_fetch_fetch_lobs_true_100k() -> None: + """Fetch 100 100 KiB Oracle CLOB rows with sqlspec LOB locators enabled.""" + _run_sqlspec_oracle_lob_fetch("100k", fetch_lobs=True) + + +async def sqlspec_oracle_lob_fetch_async_1k() -> None: + """Fetch 100 1 KiB Oracle CLOB rows with async sqlspec direct LOB fetching.""" + await _run_sqlspec_oracle_lob_fetch_async("1k", fetch_lobs=False) + + +async def sqlspec_oracle_lob_fetch_async_100k() -> None: + """Fetch 100 100 KiB Oracle CLOB rows with async sqlspec direct LOB fetching.""" + await _run_sqlspec_oracle_lob_fetch_async("100k", fetch_lobs=False) + + +async def sqlspec_oracle_lob_fetch_async_fetch_lobs_true_1k() -> None: + """Fetch 100 1 KiB Oracle CLOB rows with async sqlspec LOB locators enabled.""" + await _run_sqlspec_oracle_lob_fetch_async("1k", fetch_lobs=True) + + +async def sqlspec_oracle_lob_fetch_async_fetch_lobs_true_100k() -> None: + """Fetch 100 100 KiB Oracle CLOB rows with async sqlspec LOB locators enabled.""" + await _run_sqlspec_oracle_lob_fetch_async("100k", fetch_lobs=True) + + +def sqlspec_oracle_native_json_write() -> None: + """Write native Python mappings to an Oracle native JSON column.""" + _run_sqlspec_oracle_json_write(serialized=False) + + +def sqlspec_oracle_serialized_json_write() -> None: + """Write pre-serialized strings to an Oracle native JSON column.""" + _run_sqlspec_oracle_json_write(serialized=True) + + +def sqlspec_oracle_json_read() -> None: + """Read native JSON values from Oracle through SQLSpec.""" + _run_sqlspec_oracle_json_read() + + +def raw_sqlite_scalar_param_select() -> None: + """Execute scalar parameter SELECT queries through raw sqlite3.""" + with sqlite3.connect(":memory:") as conn: + for i in range(ROWS_TO_INSERT): + row = conn.execute(SCALAR_PARAM_SELECT_SQL, (i,)).fetchone() + assert row is not None + assert row[0] == i + + +def sqlspec_sqlite_scalar_param_select() -> None: + """Execute scalar parameter SELECT queries through SQLSpec SqliteConfig.""" + spec = SQLSpec() + config = SqliteConfig(connection_config={"database": ":memory:"}) + try: + with spec.provide_session(config) as session: + for i in range(ROWS_TO_INSERT): + val = session.select_value(SCALAR_PARAM_SELECT_SQL, i) + assert val == i + finally: + config.close_pool() + + +def raw_sqlite_no_param_select() -> None: + """Execute parameterless SELECT 1 queries through raw sqlite3.""" + with sqlite3.connect(":memory:") as conn: + for _ in range(ROWS_TO_INSERT): + rows = conn.execute(NO_PARAM_SELECT_SQL).fetchall() + assert len(rows) == 1 + + +def sqlspec_sqlite_no_param_select() -> None: + """Execute parameterless SELECT 1 queries through SQLSpec SqliteConfig.""" + spec = SQLSpec() + config = SqliteConfig(connection_config={"database": ":memory:"}) + try: + with spec.provide_session(config) as session: + for _ in range(ROWS_TO_INSERT): + res = session.execute(NO_PARAM_SELECT_SQL) + assert res is not None + finally: + config.close_pool() + + +def raw_sqlite_complex_query() -> None: + """Execute a CTE + 2-join query through raw sqlite3.""" + iterations = min(ROWS_TO_INSERT, HOT_PATH_COMPLEX_ITERATIONS_CAP) + with sqlite3.connect(":memory:") as conn: + for ddl in COMPLEX_SCHEMA_DDL: + conn.execute(ddl) + for i in range(iterations): + conn.execute(COMPLEX_QUERY_SQL, (i,)).fetchall() + + +def sqlspec_sqlite_complex_query() -> None: + """Execute a CTE + 2-join query with scalar positional param through SQLSpec SqliteConfig.""" + iterations = min(ROWS_TO_INSERT, HOT_PATH_COMPLEX_ITERATIONS_CAP) + spec = SQLSpec() + config = SqliteConfig(connection_config={"database": ":memory:"}) + try: + with spec.provide_session(config) as session: + for ddl in COMPLEX_SCHEMA_DDL: + session.execute(ddl) + for i in range(iterations): + session.execute(COMPLEX_QUERY_SQL, i) + finally: + config.close_pool() + + +def raw_sqlite_session_per_request() -> None: + """Execute one query per request on a reused raw sqlite3 connection.""" + iterations = min(ROWS_TO_INSERT, HOT_PATH_COMPLEX_ITERATIONS_CAP) + with tempfile.NamedTemporaryFile(suffix=".db") as tmp: + conn = sqlite3.connect(tmp.name) + _optimize_raw_sqlite(conn) + conn.execute(CREATE_TEST_TABLE) + conn.executemany(INSERT_TEST_VALUE, [(f"value_{i}",) for i in range(100)]) + conn.commit() + for i in range(iterations): + conn.execute(SELECT_BY_VALUE, (f"value_{i % 100}",)).fetchone() + conn.close() + + +def sqlspec_sqlite_session_per_request() -> None: + """Execute one query per provide_session context through SQLSpec SqliteConfig.""" + iterations = min(ROWS_TO_INSERT, HOT_PATH_COMPLEX_ITERATIONS_CAP) + with tempfile.NamedTemporaryFile(suffix=".db") as tmp: + spec = SQLSpec() + config = SqliteConfig(connection_config={"database": tmp.name}) + try: + with spec.provide_session(config) as session: + session.execute(CREATE_TEST_TABLE) + session.execute_many(INSERT_TEST_VALUE, [(f"value_{i}",) for i in range(100)]) + for i in range(iterations): + with spec.provide_session(config) as session: + session.fetch_one_or_none(SELECT_BY_VALUE, (f"value_{i % 100}",)) + finally: + config.close_pool() + + +def raw_sqlite_observability_select() -> None: + """Baseline raw sqlite3 repeated SELECT for observability comparison.""" + with tempfile.NamedTemporaryFile(suffix=".db") as tmp: + conn = sqlite3.connect(tmp.name) + _optimize_raw_sqlite(conn) + conn.execute(CREATE_TEST_TABLE) + conn.executemany(INSERT_TEST_VALUE, [(f"value_{i}",) for i in range(100)]) + conn.commit() + for i in range(ROWS_TO_INSERT): + conn.execute(SELECT_BY_VALUE, (f"value_{i % 100}",)).fetchone() + conn.close() + + +def sqlspec_sqlite_observability_select() -> None: + """Execute repeated SELECT queries with a statement observer attached on SqliteConfig.""" + with tempfile.NamedTemporaryFile(suffix=".db") as tmp: + spec = SQLSpec() + config = SqliteConfig( + connection_config={"database": tmp.name}, + observability_config=ObservabilityConfig(statement_observers=(_noop_statement_observer,)), + ) + try: + with spec.provide_session(config) as session: + session.execute(CREATE_TEST_TABLE) + session.execute_many(INSERT_TEST_VALUE, [(f"value_{i}",) for i in range(100)]) + for i in range(ROWS_TO_INSERT): + session.fetch_one_or_none(SELECT_BY_VALUE, (f"value_{i % 100}",)) + finally: + config.close_pool() + + +async def raw_aiosqlite_scalar_param_select() -> None: + """Execute scalar parameter SELECT queries through raw aiosqlite.""" + aiosqlite = _get_aiosqlite() + if aiosqlite is None: + return + async with aiosqlite.connect(":memory:") as conn: + for i in range(ROWS_TO_INSERT): + cursor = await conn.execute(SCALAR_PARAM_SELECT_SQL, (i,)) + row = await cursor.fetchone() + assert row is not None + assert row[0] == i + + +async def sqlspec_aiosqlite_scalar_param_select() -> None: + """Execute scalar parameter SELECT queries through SQLSpec AiosqliteConfig.""" + aiosqlite_config_type = _get_aiosqlite_config() + if aiosqlite_config_type is None: + return + spec = SQLSpec() + config = aiosqlite_config_type(connection_config={"database": ":memory:", "pool_size": POOL_SIZE}) + try: + async with spec.provide_session(config) as session: + for i in range(ROWS_TO_INSERT): + val = await session.select_value(SCALAR_PARAM_SELECT_SQL, i) + assert val == i + _check_pool_leak(config.connection_instance, "aiosqlite/scalar_param_select") + await config.close_pool() + finally: + if config.connection_instance is not None: + await config.close_pool() + + +async def raw_aiosqlite_no_param_select() -> None: + """Execute parameterless SELECT 1 queries through raw aiosqlite.""" + aiosqlite = _get_aiosqlite() + if aiosqlite is None: + return + async with aiosqlite.connect(":memory:") as conn: + for _ in range(ROWS_TO_INSERT): + cursor = await conn.execute(NO_PARAM_SELECT_SQL) + rows = await cursor.fetchall() + assert len(rows) == 1 + + +async def sqlspec_aiosqlite_no_param_select() -> None: + """Execute parameterless SELECT 1 queries through SQLSpec AiosqliteConfig.""" + aiosqlite_config_type = _get_aiosqlite_config() + if aiosqlite_config_type is None: + return + spec = SQLSpec() + config = aiosqlite_config_type(connection_config={"database": ":memory:", "pool_size": POOL_SIZE}) + try: + async with spec.provide_session(config) as session: + for _ in range(ROWS_TO_INSERT): + res = await session.execute(NO_PARAM_SELECT_SQL) + assert res is not None + _check_pool_leak(config.connection_instance, "aiosqlite/no_param_select") + await config.close_pool() + finally: + if config.connection_instance is not None: + await config.close_pool() + + +async def raw_aiosqlite_complex_query() -> None: + """Execute a CTE + 2-join query through raw aiosqlite.""" + aiosqlite = _get_aiosqlite() + if aiosqlite is None: + return + iterations = min(ROWS_TO_INSERT, HOT_PATH_COMPLEX_ITERATIONS_CAP) + async with aiosqlite.connect(":memory:") as conn: + for ddl in COMPLEX_SCHEMA_DDL: + await conn.execute(ddl) + await conn.commit() + for i in range(iterations): + cursor = await conn.execute(COMPLEX_QUERY_SQL, (i,)) + await cursor.fetchall() + + +async def sqlspec_aiosqlite_complex_query() -> None: + """Execute a CTE + 2-join query with scalar positional param through SQLSpec AiosqliteConfig.""" + aiosqlite_config_type = _get_aiosqlite_config() + if aiosqlite_config_type is None: + return + iterations = min(ROWS_TO_INSERT, HOT_PATH_COMPLEX_ITERATIONS_CAP) + spec = SQLSpec() + config = aiosqlite_config_type(connection_config={"database": ":memory:", "pool_size": POOL_SIZE}) + try: + async with spec.provide_session(config) as session: + for ddl in COMPLEX_SCHEMA_DDL: + await session.execute(ddl) + for i in range(iterations): + await session.execute(COMPLEX_QUERY_SQL, i) + _check_pool_leak(config.connection_instance, "aiosqlite/complex_query") + await config.close_pool() + finally: + if config.connection_instance is not None: + await config.close_pool() + + +async def raw_aiosqlite_session_per_request() -> None: + """Execute one query per request on a reused raw aiosqlite connection.""" + aiosqlite = _get_aiosqlite() + if aiosqlite is None: + return + iterations = min(ROWS_TO_INSERT, HOT_PATH_COMPLEX_ITERATIONS_CAP) + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: + tmp_path = Path(tmp.name) + try: + async with aiosqlite.connect(str(tmp_path)) as conn: + await conn.execute(CREATE_TEST_TABLE) + await conn.executemany(INSERT_TEST_VALUE, [(f"value_{i}",) for i in range(100)]) + await conn.commit() + for i in range(iterations): + cursor = await conn.execute(SELECT_BY_VALUE, (f"value_{i % 100}",)) + await cursor.fetchone() + finally: + with suppress(OSError): + await anyio.Path(tmp_path).unlink() + + +async def sqlspec_aiosqlite_session_per_request() -> None: + """Execute one query per provide_session context through SQLSpec AiosqliteConfig.""" + aiosqlite_config_type = _get_aiosqlite_config() + if aiosqlite_config_type is None: + return + iterations = min(ROWS_TO_INSERT, HOT_PATH_COMPLEX_ITERATIONS_CAP) + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: + tmp_path = Path(tmp.name) + try: + spec = SQLSpec() + config = aiosqlite_config_type(connection_config={"database": str(tmp_path), "pool_size": POOL_SIZE}) + try: + async with spec.provide_session(config) as session: + await session.execute(DROP_TEST_TABLE) + await session.execute(CREATE_TEST_TABLE) + await session.execute_many(INSERT_TEST_VALUE, [(f"value_{i}",) for i in range(100)]) + for i in range(iterations): + async with spec.provide_session(config) as session: + await session.fetch_one_or_none(SELECT_BY_VALUE, (f"value_{i % 100}",)) + _check_pool_leak(config.connection_instance, "aiosqlite/session_per_request") + await config.close_pool() + finally: + if config.connection_instance is not None: + await config.close_pool() + finally: + with suppress(OSError): + await anyio.Path(tmp_path).unlink() + + +async def raw_aiosqlite_observability_select() -> None: + """Baseline raw aiosqlite repeated SELECT for observability comparison.""" + aiosqlite = _get_aiosqlite() + if aiosqlite is None: + return + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: + tmp_path = Path(tmp.name) + try: + async with aiosqlite.connect(str(tmp_path)) as conn: + await conn.execute(CREATE_TEST_TABLE) + await conn.executemany(INSERT_TEST_VALUE, [(f"value_{i}",) for i in range(100)]) + await conn.commit() + for i in range(ROWS_TO_INSERT): + cursor = await conn.execute(SELECT_BY_VALUE, (f"value_{i % 100}",)) + await cursor.fetchone() + finally: + with suppress(OSError): + await anyio.Path(tmp_path).unlink() + + +async def sqlspec_aiosqlite_observability_select() -> None: + """Execute repeated SELECT queries with a statement observer attached on AiosqliteConfig.""" + aiosqlite_config_type = _get_aiosqlite_config() + if aiosqlite_config_type is None: + return + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: + tmp_path = Path(tmp.name) + try: + spec = SQLSpec() + config = aiosqlite_config_type( + connection_config={"database": str(tmp_path), "pool_size": POOL_SIZE}, + observability_config=ObservabilityConfig(statement_observers=(_noop_statement_observer,)), + ) + try: + async with spec.provide_session(config) as session: + await session.execute(DROP_TEST_TABLE) + await session.execute(CREATE_TEST_TABLE) + await session.execute_many(INSERT_TEST_VALUE, [(f"value_{i}",) for i in range(100)]) + for i in range(ROWS_TO_INSERT): + await session.fetch_one_or_none(SELECT_BY_VALUE, (f"value_{i % 100}",)) + _check_pool_leak(config.connection_instance, "aiosqlite/observability_select") + await config.close_pool() + finally: + if config.connection_instance is not None: + await config.close_pool() + finally: + with suppress(OSError): + await anyio.Path(tmp_path).unlink() + + +def raw_duckdb_scalar_param_select() -> None: + """Execute scalar parameter SELECT queries through raw duckdb.""" + duckdb = _get_duckdb() + if duckdb is None: + _benchmark_unavailable() + conn = duckdb.connect(":memory:") + try: + for i in range(ROWS_TO_INSERT): + row = conn.execute(SCALAR_PARAM_SELECT_SQL, (i,)).fetchone() + assert row is not None + assert row[0] == i + finally: + conn.close() + + +def sqlspec_duckdb_scalar_param_select() -> None: + """Execute scalar parameter SELECT queries through SQLSpec DuckDBConfig.""" + spec = SQLSpec() + config = DuckDBConfig(connection_config={"database": ":memory:"}) + try: + with spec.provide_session(config) as session: + for i in range(ROWS_TO_INSERT): + val = session.select_value(SCALAR_PARAM_SELECT_SQL, i) + assert val == i + finally: + config.close_pool() + + +def raw_duckdb_no_param_select() -> None: + """Execute parameterless SELECT 1 queries through raw duckdb.""" + duckdb = _get_duckdb() + if duckdb is None: + _benchmark_unavailable() + conn = duckdb.connect(":memory:") + try: + for _ in range(ROWS_TO_INSERT): + rows = conn.execute(NO_PARAM_SELECT_SQL).fetchall() + assert len(rows) == 1 + finally: + conn.close() + + +def sqlspec_duckdb_no_param_select() -> None: + """Execute parameterless SELECT 1 queries through SQLSpec DuckDBConfig.""" + spec = SQLSpec() + config = DuckDBConfig(connection_config={"database": ":memory:"}) + try: + with spec.provide_session(config) as session: + for _ in range(ROWS_TO_INSERT): + res = session.execute(NO_PARAM_SELECT_SQL) + assert res is not None + finally: + config.close_pool() + + +def raw_duckdb_complex_query() -> None: + """Execute a CTE + 2-join query through raw duckdb.""" + duckdb = _get_duckdb() + if duckdb is None: + _benchmark_unavailable() + iterations = min(ROWS_TO_INSERT, HOT_PATH_COMPLEX_ITERATIONS_CAP) + conn = duckdb.connect(":memory:") + try: + for ddl in COMPLEX_SCHEMA_DDL: + conn.execute(ddl) + for i in range(iterations): + conn.execute(COMPLEX_QUERY_SQL, (i,)).fetchall() + finally: + conn.close() + + +def sqlspec_duckdb_complex_query() -> None: + """Execute a CTE + 2-join query with scalar positional param through SQLSpec DuckDBConfig.""" + iterations = min(ROWS_TO_INSERT, HOT_PATH_COMPLEX_ITERATIONS_CAP) + spec = SQLSpec() + config = DuckDBConfig(connection_config={"database": ":memory:"}) + try: + with spec.provide_session(config) as session: + for ddl in COMPLEX_SCHEMA_DDL: + session.execute(ddl) + for i in range(iterations): + session.execute(COMPLEX_QUERY_SQL, i) + finally: + config.close_pool() + + +def raw_duckdb_session_per_request() -> None: + """Execute one query per request on a reused raw duckdb connection.""" + duckdb = _get_duckdb() + if duckdb is None: + _benchmark_unavailable() + iterations = min(ROWS_TO_INSERT, HOT_PATH_COMPLEX_ITERATIONS_CAP) + conn = duckdb.connect(":memory:") + try: + conn.execute(CREATE_TEST_TABLE) + conn.executemany(INSERT_TEST_VALUE, [(f"value_{i}",) for i in range(100)]) + for i in range(iterations): + conn.execute(SELECT_BY_VALUE, (f"value_{i % 100}",)).fetchone() + finally: + conn.close() + + +def sqlspec_duckdb_session_per_request() -> None: + """Execute one query per provide_session context through SQLSpec DuckDBConfig.""" + iterations = min(ROWS_TO_INSERT, HOT_PATH_COMPLEX_ITERATIONS_CAP) + spec = SQLSpec() + config = DuckDBConfig(connection_config={"database": ":memory:"}) + try: + with spec.provide_session(config) as session: + session.execute(CREATE_TEST_TABLE) + session.execute_many(INSERT_TEST_VALUE, [(f"value_{i}",) for i in range(100)]) + for i in range(iterations): + with spec.provide_session(config) as session: + session.fetch_one_or_none(SELECT_BY_VALUE, (f"value_{i % 100}",)) + finally: + config.close_pool() + + +def raw_duckdb_observability_select() -> None: + """Baseline raw duckdb repeated SELECT for observability comparison.""" + duckdb = _get_duckdb() + if duckdb is None: + _benchmark_unavailable() + conn = duckdb.connect(":memory:") + try: + conn.execute(CREATE_TEST_TABLE) + conn.executemany(INSERT_TEST_VALUE, [(f"value_{i}",) for i in range(100)]) + for i in range(ROWS_TO_INSERT): + conn.execute(SELECT_BY_VALUE, (f"value_{i % 100}",)).fetchone() + finally: + conn.close() + + +def sqlspec_duckdb_observability_select() -> None: + """Execute repeated SELECT queries with a statement observer attached on DuckDBConfig.""" + spec = SQLSpec() + config = DuckDBConfig( + connection_config={"database": ":memory:"}, + observability_config=ObservabilityConfig(statement_observers=(_noop_statement_observer,)), + ) + try: + with spec.provide_session(config) as session: + session.execute(CREATE_TEST_TABLE) + session.execute_many(INSERT_TEST_VALUE, [(f"value_{i}",) for i in range(100)]) + for i in range(ROWS_TO_INSERT): + session.fetch_one_or_none(SELECT_BY_VALUE, (f"value_{i % 100}",)) + finally: + config.close_pool() + + +async def raw_asyncpg_scalar_param_select() -> None: + """Execute scalar parameter SELECT queries through raw asyncpg.""" + await _run_raw_asyncpg_hot_path("scalar_param_select") + + +async def sqlspec_asyncpg_scalar_param_select() -> None: + """Execute scalar parameter SELECT queries through SQLSpec AsyncpgConfig.""" + await _run_sqlspec_asyncpg_hot_path("scalar_param_select") + + +async def raw_asyncpg_no_param_select() -> None: + """Execute parameterless SELECT 1 queries through raw asyncpg.""" + await _run_raw_asyncpg_hot_path("no_param_select") + + +async def sqlspec_asyncpg_no_param_select() -> None: + """Execute parameterless SELECT 1 queries through SQLSpec AsyncpgConfig.""" + await _run_sqlspec_asyncpg_hot_path("no_param_select") + + +async def raw_asyncpg_complex_query() -> None: + """Execute a CTE + 2-join query through raw asyncpg.""" + await _run_raw_asyncpg_hot_path("complex_query") + + +async def sqlspec_asyncpg_complex_query() -> None: + """Execute a CTE + 2-join query through SQLSpec AsyncpgConfig.""" + await _run_sqlspec_asyncpg_hot_path("complex_query") + + +async def raw_asyncpg_session_per_request() -> None: + """Execute one query per request on a reused raw asyncpg connection.""" + await _run_raw_asyncpg_hot_path("session_per_request") + + +async def sqlspec_asyncpg_session_per_request() -> None: + """Execute one query per provide_session context through SQLSpec AsyncpgConfig.""" + await _run_sqlspec_asyncpg_hot_path("session_per_request") + + +async def raw_asyncpg_observability_select() -> None: + """Baseline raw asyncpg repeated SELECT for observability comparison.""" + await _run_raw_asyncpg_hot_path("observability_select") + + +async def sqlspec_asyncpg_observability_select() -> None: + """Execute repeated SELECT queries with a statement observer attached on AsyncpgConfig.""" + await _run_sqlspec_asyncpg_hot_path("observability_select") + + +def raw_psycopg_sync_scalar_param_select() -> None: + """Execute scalar parameter SELECT queries through raw psycopg sync.""" + _run_raw_psycopg_sync_hot_path("scalar_param_select") + + +def sqlspec_psycopg_sync_scalar_param_select() -> None: + """Execute scalar parameter SELECT queries through SQLSpec PsycopgSyncConfig.""" + _run_sqlspec_psycopg_sync_hot_path("scalar_param_select") + + +def raw_psycopg_sync_no_param_select() -> None: + """Execute parameterless SELECT 1 queries through raw psycopg sync.""" + _run_raw_psycopg_sync_hot_path("no_param_select") + + +def sqlspec_psycopg_sync_no_param_select() -> None: + """Execute parameterless SELECT 1 queries through SQLSpec PsycopgSyncConfig.""" + _run_sqlspec_psycopg_sync_hot_path("no_param_select") + + +def raw_psycopg_sync_complex_query() -> None: + """Execute a CTE + 2-join query through raw psycopg sync.""" + _run_raw_psycopg_sync_hot_path("complex_query") + + +def sqlspec_psycopg_sync_complex_query() -> None: + """Execute a CTE + 2-join query through SQLSpec PsycopgSyncConfig.""" + _run_sqlspec_psycopg_sync_hot_path("complex_query") + + +def raw_psycopg_sync_session_per_request() -> None: + """Execute one query per request on a reused raw psycopg sync connection.""" + _run_raw_psycopg_sync_hot_path("session_per_request") + + +def sqlspec_psycopg_sync_session_per_request() -> None: + """Execute one query per provide_session context through SQLSpec PsycopgSyncConfig.""" + _run_sqlspec_psycopg_sync_hot_path("session_per_request") + + +def raw_psycopg_sync_observability_select() -> None: + """Baseline raw psycopg sync repeated SELECT for observability comparison.""" + _run_raw_psycopg_sync_hot_path("observability_select") + + +def sqlspec_psycopg_sync_observability_select() -> None: + """Execute repeated SELECT queries with a statement observer attached on PsycopgSyncConfig.""" + _run_sqlspec_psycopg_sync_hot_path("observability_select") + + +async def raw_psycopg_async_scalar_param_select() -> None: + """Execute scalar parameter SELECT queries through raw psycopg async.""" + await _run_raw_psycopg_async_hot_path("scalar_param_select") + + +async def sqlspec_psycopg_async_scalar_param_select() -> None: + """Execute scalar parameter SELECT queries through SQLSpec PsycopgAsyncConfig.""" + await _run_sqlspec_psycopg_async_hot_path("scalar_param_select") + + +async def raw_psycopg_async_no_param_select() -> None: + """Execute parameterless SELECT 1 queries through raw psycopg async.""" + await _run_raw_psycopg_async_hot_path("no_param_select") + + +async def sqlspec_psycopg_async_no_param_select() -> None: + """Execute parameterless SELECT 1 queries through SQLSpec PsycopgAsyncConfig.""" + await _run_sqlspec_psycopg_async_hot_path("no_param_select") + + +async def raw_psycopg_async_complex_query() -> None: + """Execute a CTE + 2-join query through raw psycopg async.""" + await _run_raw_psycopg_async_hot_path("complex_query") + + +async def sqlspec_psycopg_async_complex_query() -> None: + """Execute a CTE + 2-join query through SQLSpec PsycopgAsyncConfig.""" + await _run_sqlspec_psycopg_async_hot_path("complex_query") + + +async def raw_psycopg_async_session_per_request() -> None: + """Execute one query per request on a reused raw psycopg async connection.""" + await _run_raw_psycopg_async_hot_path("session_per_request") + + +async def sqlspec_psycopg_async_session_per_request() -> None: + """Execute one query per provide_session context through SQLSpec PsycopgAsyncConfig.""" + await _run_sqlspec_psycopg_async_hot_path("session_per_request") + + +async def raw_psycopg_async_observability_select() -> None: + """Baseline raw psycopg async repeated SELECT for observability comparison.""" + await _run_raw_psycopg_async_hot_path("observability_select") + + +async def sqlspec_psycopg_async_observability_select() -> None: + """Execute repeated SELECT queries with a statement observer attached on PsycopgAsyncConfig.""" + await _run_sqlspec_psycopg_async_hot_path("observability_select") + + +async def raw_psqlpy_rows() -> None: + """Fetch rows through the native psqlpy connection pool.""" + await _run_raw_psqlpy_rows(_postgres_benchmark_dsn()) + + +async def sqlspec_psqlpy_rows() -> None: + """Fetch rows through SQLSpec PsqlpyConfig.""" + await _run_sqlspec_psqlpy_rows(_postgres_benchmark_dsn()) + + +async def raw_psqlpy_scalar_param_select() -> None: + """Execute scalar parameter SELECT queries through raw psqlpy.""" + await _run_raw_psqlpy_hot_path("scalar_param_select") + + +async def sqlspec_psqlpy_scalar_param_select() -> None: + """Execute scalar parameter SELECT queries through SQLSpec PsqlpyConfig.""" + await _run_sqlspec_psqlpy_hot_path("scalar_param_select") + + +async def raw_psqlpy_no_param_select() -> None: + """Execute parameterless SELECT 1 queries through raw psqlpy.""" + await _run_raw_psqlpy_hot_path("no_param_select") + + +async def sqlspec_psqlpy_no_param_select() -> None: + """Execute parameterless SELECT 1 queries through SQLSpec PsqlpyConfig.""" + await _run_sqlspec_psqlpy_hot_path("no_param_select") + + +async def raw_psqlpy_complex_query() -> None: + """Execute a CTE + 2-join query through raw psqlpy.""" + await _run_raw_psqlpy_hot_path("complex_query") + + +async def sqlspec_psqlpy_complex_query() -> None: + """Execute a CTE + 2-join query through SQLSpec PsqlpyConfig.""" + await _run_sqlspec_psqlpy_hot_path("complex_query") + + +async def raw_psqlpy_session_per_request() -> None: + """Execute one query per request on a reused raw psqlpy connection.""" + await _run_raw_psqlpy_hot_path("session_per_request") + + +async def sqlspec_psqlpy_session_per_request() -> None: + """Execute one query per provide_session context through SQLSpec PsqlpyConfig.""" + await _run_sqlspec_psqlpy_hot_path("session_per_request") + + +async def raw_psqlpy_observability_select() -> None: + """Baseline raw psqlpy repeated SELECT for observability comparison.""" + await _run_raw_psqlpy_hot_path("observability_select") + + +async def sqlspec_psqlpy_observability_select() -> None: + """Execute repeated SELECT queries with a statement observer attached on PsqlpyConfig.""" + await _run_sqlspec_psqlpy_hot_path("observability_select") + + +def raw_oracle_scalar_param_select() -> None: + """Execute scalar parameter SELECT queries through raw python-oracledb.""" + _run_raw_oracle_hot_path("scalar_param_select") + + +def sqlspec_oracle_scalar_param_select() -> None: + """Execute scalar parameter SELECT queries through SQLSpec OracleSyncConfig.""" + _run_sqlspec_oracle_hot_path("scalar_param_select") + + +async def sqlspec_oracle_async_scalar_param_select() -> None: + """Execute scalar parameter SELECT queries through SQLSpec OracleAsyncConfig.""" + await _run_sqlspec_oracle_async_hot_path("scalar_param_select") + + +def raw_oracle_no_param_select() -> None: + """Execute parameterless SELECT 1 FROM DUAL queries through raw python-oracledb.""" + _run_raw_oracle_hot_path("no_param_select") + + +def sqlspec_oracle_no_param_select() -> None: + """Execute parameterless SELECT 1 FROM DUAL queries through SQLSpec OracleSyncConfig.""" + _run_sqlspec_oracle_hot_path("no_param_select") + + +async def sqlspec_oracle_async_no_param_select() -> None: + """Execute parameterless SELECT 1 FROM DUAL queries through SQLSpec OracleAsyncConfig.""" + await _run_sqlspec_oracle_async_hot_path("no_param_select") + + +def raw_oracle_complex_query() -> None: + """Execute a CTE + 2-join query through raw python-oracledb.""" + _run_raw_oracle_hot_path("complex_query") + + +def sqlspec_oracle_complex_query() -> None: + """Execute a CTE + 2-join query through SQLSpec OracleSyncConfig.""" + _run_sqlspec_oracle_hot_path("complex_query") + + +async def sqlspec_oracle_async_complex_query() -> None: + """Execute a CTE + 2-join query through SQLSpec OracleAsyncConfig.""" + await _run_sqlspec_oracle_async_hot_path("complex_query") + + +def raw_oracle_session_per_request() -> None: + """Execute one query per request on a reused raw python-oracledb connection.""" + _run_raw_oracle_hot_path("session_per_request") + + +def sqlspec_oracle_session_per_request() -> None: + """Execute one query per provide_session context through SQLSpec OracleSyncConfig.""" + _run_sqlspec_oracle_hot_path("session_per_request") + + +async def sqlspec_oracle_async_session_per_request() -> None: + """Execute one query per provide_session context through SQLSpec OracleAsyncConfig.""" + await _run_sqlspec_oracle_async_hot_path("session_per_request") + + +def raw_oracle_observability_select() -> None: + """Baseline raw python-oracledb repeated SELECT for observability comparison.""" + _run_raw_oracle_hot_path("observability_select") + + +def sqlspec_oracle_observability_select() -> None: + """Execute repeated SELECT queries with a statement observer attached on OracleSyncConfig.""" + _run_sqlspec_oracle_hot_path("observability_select") + + +async def sqlspec_oracle_async_observability_select() -> None: + """Execute repeated SELECT queries with a statement observer attached on OracleAsyncConfig.""" + await _run_sqlspec_oracle_async_hot_path("observability_select") + + +def print_benchmark_table(results: list[dict[str, Any]]) -> None: + console = Console() + table = Table(title="Benchmark Results") + table.add_column("Driver", style="cyan", no_wrap=True) + table.add_column("Library", style="magenta") + table.add_column("Scenario", style="green") + table.add_column("Time (s)", justify="right", style="yellow") + table.add_column("Stddev", justify="right") + table.add_column("IQR", justify="right") + table.add_column("Noise", justify="center") + table.add_column("% Slower vs Raw", justify="right", style="red") + + multi_iter = any(len(row.get("times", [])) > 1 for row in results) + + raw_times: dict[tuple[str, str], float] = {} + for row in results: + if row["library"] == "raw": + raw_times[row["driver"], row["scenario"]] = row["time"] + + for row in results: + driver = row["driver"] + scenario = row["scenario"] + lib = row["library"] + t = row["time"] + times = row.get("times", [t]) + stddev = float(row.get("stddev", 0.0)) + iqr = float(row.get("iqr", 0.0)) + noise = "yes" if row.get("noisy", False) else "" + if lib == "raw": + percent_slower = "---" + else: + raw_time = raw_times.get((driver, scenario)) + percent_slower = f"{100 * (t - raw_time) / raw_time:.1f}%" if raw_time and raw_time > 0 else "n/a" + time_str = f"{t:.4f} ({min(times):.4f}-{max(times):.4f})" if multi_iter and len(times) > 1 else f"{t:.4f}" + table.add_row(driver, lib, scenario, time_str, f"{stddev:.4f}", f"{iqr:.4f}", noise, percent_slower) + console.print(table) + + +def _benchmark_unavailable() -> NoReturn: + raise BenchmarkUnavailableError + + +def _is_compiled() -> bool: + """Detect if sqlspec driver modules are mypyc-compiled.""" + try: + from sqlspec.driver import _sync + + return hasattr(_sync, "__file__") and (_sync.__file__ or "").endswith(".so") + except ImportError: + return False + + +def _new_benchmark_loop() -> asyncio.AbstractEventLoop: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + return loop + + +def _close_benchmark_loop(loop: asyncio.AbstractEventLoop) -> None: + try: + loop.run_until_complete(loop.shutdown_asyncgens()) + if hasattr(loop, "shutdown_default_executor"): + loop.run_until_complete(loop.shutdown_default_executor()) + finally: + asyncio.set_event_loop(None) + loop.close() + + +def _run_scenario_once(func: Any, loop: asyncio.AbstractEventLoop | None) -> None: + if loop is not None: + loop.run_until_complete(func()) + else: + func() + + +def _run_benchmark_iterations( + func: Any, *, is_async: bool, iterations: int, warmup: int, profiler: cProfile.Profile | None = None +) -> list[float]: + loop = _new_benchmark_loop() if is_async else None + try: + for _ in range(warmup): + _run_scenario_once(func, loop) + + times: list[float] = [] + for _ in range(iterations): + gc.collect() + gc_was_enabled = gc.isenabled() + if gc_was_enabled: + gc.disable() + start = time.perf_counter() + try: + if profiler is not None: + profiler.enable() + _run_scenario_once(func, loop) + finally: + if profiler is not None: + profiler.disable() + if gc_was_enabled: + gc.enable() + times.append(time.perf_counter() - start) + return times + finally: + if loop is not None: + _close_benchmark_loop(loop) + + +def _summarize_times(times: list[float]) -> dict[str, float | bool]: + median_time = statistics.median(times) + stddev = statistics.stdev(times) if len(times) > 1 else 0.0 + quartiles = statistics.quantiles(times, n=4, method="inclusive") if len(times) > 1 else (0.0, 0.0, 0.0) + iqr = quartiles[2] - quartiles[0] + noise_ratio = stddev / median_time if median_time > 0 else 0.0 + return { + "time": median_time, + "stddev": stddev, + "iqr": iqr, + "noise_ratio": noise_ratio, + "noisy": noise_ratio > NOISY_STDDEV_RATIO, + } + + +def _benchmark_library_label(library: str) -> str: + """Return a display label for a benchmark library key.""" + if library == "sqlspec": + return SQLSPEC_LABEL + if library == "sqlspec_fetch_lobs_true": + return f"{SQLSPEC_LABEL} fetch_lobs=True" + if library == "sqlspec_async": + return f"{SQLSPEC_LABEL} async" + if library == "sqlspec_async_fetch_lobs_true": + return f"{SQLSPEC_LABEL} async fetch_lobs=True" + return library + + +def _driver_has_core_scenarios(driver: str) -> bool: + """Return True when a driver has any core benchmark scenario registered.""" + return any( + (library, driver, scenario) in SCENARIO_REGISTRY for scenario in CORE_SCENARIOS for library in CORE_LIBRARIES + ) + + +def _collect_environment_metadata() -> dict[str, Any]: + """Collect Python, platform, and key package versions for benchmark reproducibility.""" + packages: dict[str, str | None] = {} + for pkg_name in ("duckdb", "sqlglot", "aiosqlite", "pyarrow", "sqlspec"): + try: + packages[pkg_name] = importlib.metadata.version(pkg_name) + except importlib.metadata.PackageNotFoundError: + packages[pkg_name] = None + return {"python_version": platform.python_version(), "platform": platform.platform(), "packages": packages} + + +def _write_json_results( + results: list[dict[str, Any]], + output_path: str, + *, + rows: int, + pool_size: int, + iterations: int, + warmup: int = DEFAULT_BENCH_WARMUP, +) -> None: + """Write benchmark results to a JSON file. + + Args: + results: List of benchmark result dictionaries + output_path: Path to write the JSON file + rows: Number of rows used in the benchmark + pool_size: Pool size used in the benchmark + iterations: Number of timed iterations per scenario + warmup: Number of warmup iterations per scenario + """ + output = { + "metadata": { + "rows": rows, + "pool_size": pool_size, + "iterations": iterations, + "warmup": warmup, + "mypyc_compiled": _is_compiled(), + "environment": _collect_environment_metadata(), + "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z"), + }, + "results": [ + { + "driver": r["driver"], + "library": r["library"], + "library_key": r.get("library_key", r["library"]), + "scenario": r["scenario"], + "time": r["time"], + "times": r.get("times", [r["time"]]), + "stddev": r.get("stddev", 0.0), + "iqr": r.get("iqr", 0.0), + "noise_ratio": r.get("noise_ratio", 0.0), + "noisy": r.get("noisy", False), + } + for r in results + ], + } + with Path(output_path).open("w") as f: + json.dump(output, f, indent=2) + + +def _optimize_raw_sqlite(conn: sqlite3.Connection) -> None: + """Apply same PRAGMAs that sqlspec applies for fair comparison.""" + conn.execute("PRAGMA journal_mode = WAL") + conn.execute("PRAGMA synchronous = NORMAL") + conn.execute("PRAGMA busy_timeout = 5000") + conn.execute("PRAGMA foreign_keys = ON") + + +def _get_sqlalchemy() -> tuple[Any, Any]: + """Import SQLAlchemy lazily.""" + try: + from sqlalchemy import create_engine, text + except ImportError: + return None, None + else: + return create_engine, text + + +def _get_duckdb() -> Any: + """Import duckdb lazily.""" + try: + import duckdb + except ImportError: + return None + else: + return duckdb + + +def _get_adbc_dbapi() -> Any: + """Import the ADBC DB-API lazily.""" + try: + from adbc_driver_manager import dbapi + except ImportError: + return None + else: + return dbapi + + +def _get_adbc_config() -> Any: + """Import AdbcConfig lazily.""" + try: + from sqlspec.adapters.adbc import AdbcConfig + except ImportError: + return None + else: + return AdbcConfig + + +def _adbc_benchmark_connection_config() -> dict[str, Any] | None: + """Build an ADBC benchmark connection from service-provided environment values.""" + uri = os.environ.get("SQLSPEC_BENCH_ADBC_URI") + driver_name = os.environ.get("SQLSPEC_BENCH_ADBC_DRIVER_NAME") + if not uri or not driver_name: + return None + config: dict[str, Any] = {"uri": uri, "driver_name": driver_name} + for env_name, config_name in ( + ("SQLSPEC_BENCH_ADBC_USERNAME", "username"), + ("SQLSPEC_BENCH_ADBC_PASSWORD", "password"), + ("SQLSPEC_BENCH_ADBC_GIZMOSQL_BACKEND", "gizmosql_backend"), + ): + value = os.environ.get(env_name) + if value: + config[config_name] = value + if os.environ.get("SQLSPEC_BENCH_ADBC_TLS_SKIP_VERIFY") == "1": + config["tls_skip_verify"] = True + return config + + +def _adbc_benchmark_rows() -> list[tuple[int, str]]: + """Return the stable row payload used by the ADBC benchmark.""" + return [(index, f"value_{index}") for index in range(BENCHMARK_ROWS)] + + +def _connect_adbc(dbapi: Any, config: dict[str, Any]) -> Any: + driver_name = config["driver_name"] + if driver_name in {"adbc_driver_sqlite", "adbc_driver_duckdb", "adbc_driver_postgresql"}: + module = importlib.import_module(f"{driver_name}.dbapi") + return module.connect(uri=config["uri"], autocommit=True) + db_kwargs = { + key: config[key] for key in ("username", "password", "gizmosql_backend", "tls_skip_verify") if key in config + } + return dbapi.connect(driver=driver_name, uri=config["uri"], db_kwargs=db_kwargs, autocommit=True) + + +def _get_mysql_connector() -> Any: + """Import mysql-connector lazily.""" + try: + import mysql.connector + except ImportError: + return None + else: + return mysql.connector + + +def _get_mysql_connector_config() -> Any: + """Import MysqlConnectorSyncConfig lazily.""" + try: + from sqlspec.adapters.mysqlconnector import MysqlConnectorSyncConfig + except ImportError: + return None + else: + return MysqlConnectorSyncConfig + + +def _mysql_benchmark_connection_config() -> dict[str, Any] | None: + """Build a MySQL benchmark connection from service-provided environment values.""" + required: dict[str, Any] = { + "host": os.environ.get("SQLSPEC_BENCH_MYSQL_HOST"), + "port": os.environ.get("SQLSPEC_BENCH_MYSQL_PORT"), + "user": os.environ.get("SQLSPEC_BENCH_MYSQL_USER"), + "password": os.environ.get("SQLSPEC_BENCH_MYSQL_PASSWORD"), + "database": os.environ.get("SQLSPEC_BENCH_MYSQL_DATABASE"), + } + if any(value is None for value in required.values()): + return None + required["port"] = int(required["port"]) + required["use_pure"] = True + required["autocommit"] = True + return required + + +def _json_benchmark_rows() -> list[tuple[int, str]]: + """Return the stable JSON payload used by the MySQL benchmark.""" + return [(index, json.dumps({"index": index, "values": [index, index + 1]})) for index in range(BENCHMARK_ROWS)] + + +def _json_row_payload(row: Any) -> Any: + payload = row.get("payload") if isinstance(row, dict) else row[1] + return json.loads(payload) if isinstance(payload, str) else payload + + +def _spanner_benchmark_config() -> dict[str, Any] | None: + """Build a Spanner benchmark connection from service-provided environment values.""" + required: dict[str, Any] = { + "project": os.environ.get("SQLSPEC_BENCH_SPANNER_PROJECT"), + "instance_id": os.environ.get("SQLSPEC_BENCH_SPANNER_INSTANCE_ID"), + "database_id": os.environ.get("SQLSPEC_BENCH_SPANNER_DATABASE_ID"), + "api_endpoint": os.environ.get("SQLSPEC_BENCH_SPANNER_API_ENDPOINT"), + } + if any(value is None for value in required.values()): + return None + return required + + +def _get_duckdb_engine() -> tuple[Any, Any]: + """Import SQLAlchemy with duckdb_engine lazily.""" + try: + importlib.import_module("duckdb_engine") + from sqlalchemy import create_engine, text + except ImportError: + return None, None + else: + return create_engine, text + + +def _check_pool_leak(pool: Any, scenario_name: str) -> None: + """Check for connection leaks in a pool. + + Args: + pool: Connection pool with size() and checked_out() methods + scenario_name: Name of the scenario for error reporting + """ + if pool is None: + return + + with suppress(AttributeError, TypeError): + total = pool.size() + checked_out = pool.checked_out() + if checked_out > 0: + _leaked_pools.append(f"{scenario_name}: {checked_out}/{total} connections leaked") + + +def _get_aiosqlite() -> Any: + """Import aiosqlite lazily.""" + try: + import aiosqlite + except ImportError: + return None + else: + return aiosqlite + + +def _get_aiosqlite_config() -> Any: + """Import AiosqliteConfig lazily.""" + try: + from sqlspec.adapters.aiosqlite import AiosqliteConfig + except ImportError: + return None + else: + return AiosqliteConfig + + +def _get_asyncpg() -> Any: + """Import asyncpg lazily.""" + try: + from asyncpg import connect + except ImportError: + return None + else: + return connect + + +def _get_asyncpg_config() -> Any: + """Import AsyncpgConfig lazily.""" + try: + from sqlspec.adapters.asyncpg import AsyncpgConfig + except ImportError: + return None + else: + return AsyncpgConfig + + +def _get_async_sqlalchemy() -> tuple[Any, Any]: + """Import async SQLAlchemy lazily.""" + try: + from sqlalchemy import text + from sqlalchemy.ext.asyncio import create_async_engine + except ImportError: + return None, None + else: + return create_async_engine, text + + +def _postgres_benchmark_dsn() -> str: + dsn = os.environ.get(POSTGRES_DSN_ENV) + if not dsn: + _benchmark_unavailable() + return dsn + + +def _cockroach_benchmark_dsn() -> str: + dsn = os.environ.get(COCKROACH_DSN_ENV) + if not dsn: + _benchmark_unavailable() + return dsn + + +def _asyncpg_connect_kwargs(dsn: str) -> dict[str, Any]: + """Adapt a service DSN to asyncpg's keyword set.""" + parsed = urlsplit(dsn) + query = dict(parse_qsl(parsed.query, keep_blank_values=True)) + sslmode = query.pop("sslmode", None) + clean_dsn = urlunsplit((parsed.scheme, parsed.netloc, parsed.path, urlencode(query), parsed.fragment)) + kwargs: dict[str, Any] = {"dsn": clean_dsn} + if sslmode == "disable": + kwargs["ssl"] = False + return kwargs + + +def _service_benchmark_rows() -> list[tuple[int, str]]: + return [(index, f"value_{index}") for index in range(BENCHMARK_ROWS)] + + +def _get_psycopg() -> Any: + try: + import psycopg + except ImportError: + return None + else: + return psycopg + + +def _run_raw_psycopg_sync_rows(dsn: str, *, cockroach: bool) -> None: + psycopg = _get_psycopg() + if psycopg is None: + _benchmark_unavailable() + if cockroach: + from psycopg import crdb + + connect = crdb.connect + else: + connect = psycopg.connect + with connect(dsn) as connection: + with connection.cursor() as cursor: + cursor.execute(SERVICE_ROWS_DROP) + cursor.execute(SERVICE_ROWS_CREATE) + cursor.executemany(SERVICE_ROWS_INSERT_PG, _service_benchmark_rows()) + cursor.execute(SERVICE_ROWS_SELECT) + rows = cursor.fetchall() + connection.commit() + assert len(rows) == BENCHMARK_ROWS + + +def _get_sync_service_config(driver: str) -> Any: + try: + if driver == "psycopg_sync": + from sqlspec.adapters.psycopg import PsycopgSyncConfig + + return PsycopgSyncConfig + from sqlspec.adapters.cockroach_psycopg import CockroachPsycopgSyncConfig + except ImportError: + return None + else: + return CockroachPsycopgSyncConfig + + +def _run_sqlspec_sync_service_rows(dsn: str, *, driver: str) -> None: + config_class = _get_sync_service_config(driver) + if config_class is None: + _benchmark_unavailable() + config = config_class(connection_config={"conninfo": dsn, "min_size": 1, "max_size": max(1, POOL_SIZE)}) + try: + spec = SQLSpec() + with spec.provide_session(config) as session: + session.execute(SERVICE_ROWS_DROP) + session.execute(SERVICE_ROWS_CREATE) + session.execute_many(SERVICE_ROWS_INSERT_PG, _service_benchmark_rows()) + rows = session.fetch(SERVICE_ROWS_SELECT) + assert len(rows) == BENCHMARK_ROWS + finally: + config.close_pool() + + +async def _run_raw_psycopg_async_rows(dsn: str) -> None: + psycopg = _get_psycopg() + if psycopg is None: + _benchmark_unavailable() + connection = await psycopg.AsyncConnection.connect(dsn) + try: + async with connection.cursor() as cursor: + await cursor.execute(SERVICE_ROWS_DROP) + await cursor.execute(SERVICE_ROWS_CREATE) + await cursor.executemany(SERVICE_ROWS_INSERT_PG, _service_benchmark_rows()) + await cursor.execute(SERVICE_ROWS_SELECT) + rows = await cursor.fetchall() + await connection.commit() + finally: + await connection.close() + assert len(rows) == BENCHMARK_ROWS + + +async def _run_raw_asyncpg_rows(dsn: str) -> None: + connect = _get_asyncpg() + if connect is None: + _benchmark_unavailable() + connection = await connect(**_asyncpg_connect_kwargs(dsn)) + try: + await connection.execute(SERVICE_ROWS_DROP) + await connection.execute(SERVICE_ROWS_CREATE) + await connection.executemany(SERVICE_ROWS_INSERT_ASYNCPG, _service_benchmark_rows()) + rows = await connection.fetch(SERVICE_ROWS_SELECT) + finally: + await connection.close() + assert len(rows) == BENCHMARK_ROWS + + +def _get_async_service_config(driver: str) -> Any: + try: + if driver in {"psycopg_async", "cockroach_psycopg_async"}: + if driver == "psycopg_async": + from sqlspec.adapters.psycopg import PsycopgAsyncConfig + + return PsycopgAsyncConfig + from sqlspec.adapters.cockroach_psycopg import CockroachPsycopgAsyncConfig + + return CockroachPsycopgAsyncConfig + if driver == "asyncpg": + from sqlspec.adapters.asyncpg import AsyncpgConfig + + return AsyncpgConfig + from sqlspec.adapters.cockroach_asyncpg import CockroachAsyncpgConfig + except ImportError: + return None + else: + return CockroachAsyncpgConfig + + +async def _run_sqlspec_async_service_rows(dsn: str, *, driver: str) -> None: + config_class = _get_async_service_config(driver) + if config_class is None: + _benchmark_unavailable() + connection_key = "dsn" if "asyncpg" in driver else "conninfo" + config = config_class(connection_config={connection_key: dsn, "min_size": 1, "max_size": max(1, POOL_SIZE)}) + try: + spec = SQLSpec() + async with spec.provide_session(config) as session: + await session.execute(SERVICE_ROWS_DROP) + await session.execute(SERVICE_ROWS_CREATE) + insert_sql = SERVICE_ROWS_INSERT_ASYNCPG if "asyncpg" in driver else SERVICE_ROWS_INSERT_PG + await session.execute_many(insert_sql, _service_benchmark_rows()) + rows = await session.fetch(SERVICE_ROWS_SELECT) + assert len(rows) == BENCHMARK_ROWS + finally: + await config.close_pool() + + +def _schema_type_numpy_payload() -> list[dict[str, int]]: + """Return the stable 5000x5 payload used by the schema_type numpy benchmark.""" + global _SCHEMA_TYPE_NUMPY_PAYLOAD + if _SCHEMA_TYPE_NUMPY_PAYLOAD is None: + _SCHEMA_TYPE_NUMPY_PAYLOAD = [ + {"field_0": index, "field_1": index + 1, "field_2": index + 2, "field_3": index + 3, "field_4": index + 4} + for index in range(SCHEMA_TYPE_NUMPY_ROW_COUNT) + ] + return _SCHEMA_TYPE_NUMPY_PAYLOAD + + +def _schema_type_numpy_row_type() -> type[Any]: + """Return the cached msgspec row type used by the schema_type numpy benchmark.""" + global _SCHEMA_TYPE_NUMPY_ROW_TYPE + if _SCHEMA_TYPE_NUMPY_ROW_TYPE is None: + import msgspec + + class SchemaTypeNumpyRow(msgspec.Struct): + field_0: int + field_1: int + field_2: int + field_3: int + field_4: int + + _SCHEMA_TYPE_NUMPY_ROW_TYPE = SchemaTypeNumpyRow + return _SCHEMA_TYPE_NUMPY_ROW_TYPE + +def _schema_type_numpy_vector_type() -> type[Any]: + """Return the cached msgspec vector row type used to assert ndarray fallback behavior.""" + global _SCHEMA_TYPE_NUMPY_VECTOR_TYPE + if _SCHEMA_TYPE_NUMPY_VECTOR_TYPE is None: + import msgspec -def raw_sqlite_thin_path_stress() -> None: - """100K individual INSERTs to stress-test per-call execution path (raw).""" - stress_count = ROWS_TO_INSERT * 10 - with tempfile.NamedTemporaryFile(suffix=".db") as tmp: - conn = sqlite3.connect(tmp.name) - _optimize_raw_sqlite(conn) - conn.execute(CREATE_TEST_TABLE) - for i in range(stress_count): - conn.execute(INSERT_TEST_VALUE, (f"value_{i}",)) - conn.commit() - conn.close() + class SchemaTypeNumpyVectorRow(msgspec.Struct): + values: list[float] + _SCHEMA_TYPE_NUMPY_VECTOR_TYPE = SchemaTypeNumpyVectorRow + return _SCHEMA_TYPE_NUMPY_VECTOR_TYPE -def sqlspec_sqlite_thin_path_stress() -> None: - """100K individual INSERTs to stress-test the thin execute path (sqlspec).""" - stress_count = ROWS_TO_INSERT * 10 - with tempfile.NamedTemporaryFile(suffix=".db") as tmp: - spec = SQLSpec() - config = SqliteConfig(connection_config={"database": tmp.name}) - with spec.provide_session(config) as session: - session.execute(CREATE_TEST_TABLE) - for i in range(stress_count): - session.execute(INSERT_TEST_VALUE, (f"value_{i}",)) +def _generate_wide_row(i: int) -> tuple[str, ...]: + """Generate a single row tuple for the wide_test table.""" + return ( + f"First{i}", + f"Last{i}", + f"user{i}@example.com", + f"+1-555-{i:04d}", + f"{i} Main Street", + f"{10000 + i}", + "United States", + f"199{i % 10}-{1 + (i % 12):02d}-{1 + (i % 28):02d}", + f"{100.0 + i * 0.5:.2f}", + "true" if i % 2 == 0 else "false", + ) -# --- Oracle LOB fetch scenarios --- -ORACLE_LOB_TABLES = {"1k": "SQLSPEC_LOB_1K", "100k": "SQLSPEC_LOB_100K"} -ORACLE_JSON_TABLE = "SQLSPEC_JSON_BENCH" +def _generate_complex_row(i: int) -> tuple[str, str, str]: + """Generate a single row tuple for the complex_test table.""" + import uuid as uuid_mod + + uid = str(uuid_mod.UUID(int=i)) + created_at = f"2024-{1 + (i % 12):02d}-{1 + (i % 28):02d}T{i % 24:02d}:{i % 60:02d}:00" + payload = f'{{"key": "value_{i}", "count": {i}, "tags": ["a", "b"]}}' + return (uid, created_at, payload) def _get_oracledb() -> Any: @@ -2816,73 +3915,578 @@ def _run_sqlspec_oracle_json_read() -> None: config.close_pool() -def raw_oracle_lob_fetch_1k() -> None: - """Fetch 100 1 KiB Oracle CLOB rows through raw python-oracledb.""" - _run_raw_oracle_lob_fetch("1k") +def _noop_statement_observer(_event: Any) -> None: + """No-op statement observer used for observability benchmark scenarios.""" + return -def raw_oracle_lob_fetch_100k() -> None: - """Fetch 100 100 KiB Oracle CLOB rows through raw python-oracledb.""" - _run_raw_oracle_lob_fetch("100k") +def _service_loop_iterations() -> int: + """Return bounded iteration count for network-backed hot-path scenarios.""" + return min(ROWS_TO_INSERT, HOT_PATH_COMPLEX_ITERATIONS_CAP) -def sqlspec_oracle_lob_fetch_1k() -> None: - """Fetch 100 1 KiB Oracle CLOB rows with sqlspec direct LOB fetching.""" - _run_sqlspec_oracle_lob_fetch("1k", fetch_lobs=False) +async def _run_raw_asyncpg_hot_path(scenario: str) -> None: + connect = _get_asyncpg() + if connect is None: + _benchmark_unavailable() + dsn = _postgres_benchmark_dsn() + iterations = _service_loop_iterations() + conn = await connect(**_asyncpg_connect_kwargs(dsn)) + try: + if scenario == "scalar_param_select": + for i in range(iterations): + val = await conn.fetchval(SCALAR_PARAM_SELECT_ASYNCPG_SQL, i) + assert val == i + elif scenario == "no_param_select": + for _ in range(iterations): + rows = await conn.fetch(NO_PARAM_SELECT_SQL) + assert len(rows) == 1 + elif scenario == "complex_query": + for ddl in POSTGRES_COMPLEX_DROP_DDL: + await conn.execute(ddl) + for ddl in POSTGRES_COMPLEX_SCHEMA_DDL: + await conn.execute(ddl) + for i in range(iterations): + await conn.fetch(POSTGRES_COMPLEX_QUERY_ASYNCPG_SQL, i) + elif scenario in {"session_per_request", "observability_select"}: + await conn.execute(SERVICE_ROWS_DROP) + await conn.execute(SERVICE_ROWS_CREATE) + await conn.executemany(SERVICE_ROWS_INSERT_ASYNCPG, [(i, f"value_{i}") for i in range(100)]) + for i in range(iterations): + await conn.fetchrow(SERVICE_ROWS_SELECT_BY_PAYLOAD_ASYNCPG, f"value_{i % 100}") + finally: + await conn.close() -def sqlspec_oracle_lob_fetch_100k() -> None: - """Fetch 100 100 KiB Oracle CLOB rows with sqlspec direct LOB fetching.""" - _run_sqlspec_oracle_lob_fetch("100k", fetch_lobs=False) +async def _run_sqlspec_asyncpg_hot_path(scenario: str) -> None: + asyncpg_config_type = _get_asyncpg_config() + if asyncpg_config_type is None: + _benchmark_unavailable() + dsn = _postgres_benchmark_dsn() + iterations = _service_loop_iterations() + spec = SQLSpec() + obs_cfg = ( + ObservabilityConfig(statement_observers=(_noop_statement_observer,)) + if scenario == "observability_select" + else None + ) + config = asyncpg_config_type( + connection_config={"dsn": dsn, "min_size": 1, "max_size": max(1, POOL_SIZE)}, observability_config=obs_cfg + ) + try: + if scenario == "scalar_param_select": + async with spec.provide_session(config) as session: + for i in range(iterations): + val = await session.select_value(SCALAR_PARAM_SELECT_ASYNCPG_SQL, i) + assert val == i + elif scenario == "no_param_select": + async with spec.provide_session(config) as session: + for _ in range(iterations): + res = await session.execute(NO_PARAM_SELECT_SQL) + assert res is not None + elif scenario == "complex_query": + async with spec.provide_session(config) as session: + for ddl in POSTGRES_COMPLEX_DROP_DDL: + await session.execute(ddl) + for ddl in POSTGRES_COMPLEX_SCHEMA_DDL: + await session.execute(ddl) + for i in range(iterations): + await session.execute(POSTGRES_COMPLEX_QUERY_ASYNCPG_SQL, i) + elif scenario == "session_per_request": + async with spec.provide_session(config) as session: + await session.execute(SERVICE_ROWS_DROP) + await session.execute(SERVICE_ROWS_CREATE) + await session.execute_many(SERVICE_ROWS_INSERT_ASYNCPG, [(i, f"value_{i}") for i in range(100)]) + for i in range(iterations): + async with spec.provide_session(config) as session: + await session.fetch_one_or_none(SERVICE_ROWS_SELECT_BY_PAYLOAD_ASYNCPG, (f"value_{i % 100}",)) + elif scenario == "observability_select": + async with spec.provide_session(config) as session: + await session.execute(SERVICE_ROWS_DROP) + await session.execute(SERVICE_ROWS_CREATE) + await session.execute_many(SERVICE_ROWS_INSERT_ASYNCPG, [(i, f"value_{i}") for i in range(100)]) + for i in range(iterations): + await session.fetch_one_or_none(SERVICE_ROWS_SELECT_BY_PAYLOAD_ASYNCPG, (f"value_{i % 100}",)) + finally: + await config.close_pool() -def sqlspec_oracle_lob_fetch_fetch_lobs_true_1k() -> None: - """Fetch 100 1 KiB Oracle CLOB rows with sqlspec LOB locators enabled.""" - _run_sqlspec_oracle_lob_fetch("1k", fetch_lobs=True) +def _run_raw_psycopg_sync_hot_path(scenario: str) -> None: + psycopg = _get_psycopg() + if psycopg is None: + _benchmark_unavailable() + dsn = _postgres_benchmark_dsn() + iterations = _service_loop_iterations() + with psycopg.connect(dsn) as conn, conn.cursor() as cursor: + if scenario == "scalar_param_select": + for i in range(iterations): + cursor.execute(SCALAR_PARAM_SELECT_PG_SQL, (i,)) + row = cursor.fetchone() + assert row is not None + assert row[0] == i + elif scenario == "no_param_select": + for _ in range(iterations): + cursor.execute(NO_PARAM_SELECT_SQL) + rows = cursor.fetchall() + assert len(rows) == 1 + elif scenario == "complex_query": + for ddl in POSTGRES_COMPLEX_DROP_DDL: + cursor.execute(ddl) + for ddl in POSTGRES_COMPLEX_SCHEMA_DDL: + cursor.execute(ddl) + conn.commit() + for i in range(iterations): + cursor.execute(POSTGRES_COMPLEX_QUERY_PG_SQL, (i,)) + cursor.fetchall() + elif scenario in {"session_per_request", "observability_select"}: + cursor.execute(SERVICE_ROWS_DROP) + cursor.execute(SERVICE_ROWS_CREATE) + cursor.executemany(SERVICE_ROWS_INSERT_PG, [(i, f"value_{i}") for i in range(100)]) + conn.commit() + for i in range(iterations): + cursor.execute(SERVICE_ROWS_SELECT_BY_PAYLOAD_PG, (f"value_{i % 100}",)) + cursor.fetchone() -def sqlspec_oracle_lob_fetch_fetch_lobs_true_100k() -> None: - """Fetch 100 100 KiB Oracle CLOB rows with sqlspec LOB locators enabled.""" - _run_sqlspec_oracle_lob_fetch("100k", fetch_lobs=True) +def _run_sqlspec_psycopg_sync_hot_path(scenario: str) -> None: + config_class = _get_sync_service_config("psycopg_sync") + if config_class is None: + _benchmark_unavailable() + dsn = _postgres_benchmark_dsn() + iterations = _service_loop_iterations() + spec = SQLSpec() + obs_cfg = ( + ObservabilityConfig(statement_observers=(_noop_statement_observer,)) + if scenario == "observability_select" + else None + ) + config = config_class( + connection_config={"conninfo": dsn, "min_size": 1, "max_size": max(1, POOL_SIZE)}, observability_config=obs_cfg + ) + try: + if scenario == "scalar_param_select": + with spec.provide_session(config) as session: + for i in range(iterations): + val = session.select_value(SCALAR_PARAM_SELECT_PG_SQL, i) + assert val == i + elif scenario == "no_param_select": + with spec.provide_session(config) as session: + for _ in range(iterations): + res = session.execute(NO_PARAM_SELECT_SQL) + assert res is not None + elif scenario == "complex_query": + with spec.provide_session(config) as session: + for ddl in POSTGRES_COMPLEX_DROP_DDL: + session.execute(ddl) + for ddl in POSTGRES_COMPLEX_SCHEMA_DDL: + session.execute(ddl) + for i in range(iterations): + session.execute(POSTGRES_COMPLEX_QUERY_PG_SQL, i) + elif scenario == "session_per_request": + with spec.provide_session(config) as session: + session.execute(SERVICE_ROWS_DROP) + session.execute(SERVICE_ROWS_CREATE) + session.execute_many(SERVICE_ROWS_INSERT_PG, [(i, f"value_{i}") for i in range(100)]) + for i in range(iterations): + with spec.provide_session(config) as session: + session.fetch_one_or_none(SERVICE_ROWS_SELECT_BY_PAYLOAD_PG, (f"value_{i % 100}",)) + elif scenario == "observability_select": + with spec.provide_session(config) as session: + session.execute(SERVICE_ROWS_DROP) + session.execute(SERVICE_ROWS_CREATE) + session.execute_many(SERVICE_ROWS_INSERT_PG, [(i, f"value_{i}") for i in range(100)]) + for i in range(iterations): + session.fetch_one_or_none(SERVICE_ROWS_SELECT_BY_PAYLOAD_PG, (f"value_{i % 100}",)) + finally: + config.close_pool() -async def sqlspec_oracle_lob_fetch_async_1k() -> None: - """Fetch 100 1 KiB Oracle CLOB rows with async sqlspec direct LOB fetching.""" - await _run_sqlspec_oracle_lob_fetch_async("1k", fetch_lobs=False) +async def _run_raw_psycopg_async_hot_path(scenario: str) -> None: + psycopg = _get_psycopg() + if psycopg is None: + _benchmark_unavailable() + dsn = _postgres_benchmark_dsn() + iterations = _service_loop_iterations() + conn = await psycopg.AsyncConnection.connect(dsn) + try: + async with conn.cursor() as cursor: + if scenario == "scalar_param_select": + for i in range(iterations): + await cursor.execute(SCALAR_PARAM_SELECT_PG_SQL, (i,)) + row = await cursor.fetchone() + assert row is not None + assert row[0] == i + elif scenario == "no_param_select": + for _ in range(iterations): + await cursor.execute(NO_PARAM_SELECT_SQL) + rows = await cursor.fetchall() + assert len(rows) == 1 + elif scenario == "complex_query": + for ddl in POSTGRES_COMPLEX_DROP_DDL: + await cursor.execute(ddl) + for ddl in POSTGRES_COMPLEX_SCHEMA_DDL: + await cursor.execute(ddl) + await conn.commit() + for i in range(iterations): + await cursor.execute(POSTGRES_COMPLEX_QUERY_PG_SQL, (i,)) + await cursor.fetchall() + elif scenario in {"session_per_request", "observability_select"}: + await cursor.execute(SERVICE_ROWS_DROP) + await cursor.execute(SERVICE_ROWS_CREATE) + await cursor.executemany(SERVICE_ROWS_INSERT_PG, [(i, f"value_{i}") for i in range(100)]) + await conn.commit() + for i in range(iterations): + await cursor.execute(SERVICE_ROWS_SELECT_BY_PAYLOAD_PG, (f"value_{i % 100}",)) + await cursor.fetchone() + finally: + await conn.close() -async def sqlspec_oracle_lob_fetch_async_100k() -> None: - """Fetch 100 100 KiB Oracle CLOB rows with async sqlspec direct LOB fetching.""" - await _run_sqlspec_oracle_lob_fetch_async("100k", fetch_lobs=False) +async def _run_sqlspec_psycopg_async_hot_path(scenario: str) -> None: + config_class = _get_async_service_config("psycopg_async") + if config_class is None: + _benchmark_unavailable() + dsn = _postgres_benchmark_dsn() + iterations = _service_loop_iterations() + spec = SQLSpec() + obs_cfg = ( + ObservabilityConfig(statement_observers=(_noop_statement_observer,)) + if scenario == "observability_select" + else None + ) + config = config_class( + connection_config={"conninfo": dsn, "min_size": 1, "max_size": max(1, POOL_SIZE)}, observability_config=obs_cfg + ) + try: + if scenario == "scalar_param_select": + async with spec.provide_session(config) as session: + for i in range(iterations): + val = await session.select_value(SCALAR_PARAM_SELECT_PG_SQL, i) + assert val == i + elif scenario == "no_param_select": + async with spec.provide_session(config) as session: + for _ in range(iterations): + res = await session.execute(NO_PARAM_SELECT_SQL) + assert res is not None + elif scenario == "complex_query": + async with spec.provide_session(config) as session: + for ddl in POSTGRES_COMPLEX_DROP_DDL: + await session.execute(ddl) + for ddl in POSTGRES_COMPLEX_SCHEMA_DDL: + await session.execute(ddl) + for i in range(iterations): + await session.execute(POSTGRES_COMPLEX_QUERY_PG_SQL, i) + elif scenario == "session_per_request": + async with spec.provide_session(config) as session: + await session.execute(SERVICE_ROWS_DROP) + await session.execute(SERVICE_ROWS_CREATE) + await session.execute_many(SERVICE_ROWS_INSERT_PG, [(i, f"value_{i}") for i in range(100)]) + for i in range(iterations): + async with spec.provide_session(config) as session: + await session.fetch_one_or_none(SERVICE_ROWS_SELECT_BY_PAYLOAD_PG, (f"value_{i % 100}",)) + elif scenario == "observability_select": + async with spec.provide_session(config) as session: + await session.execute(SERVICE_ROWS_DROP) + await session.execute(SERVICE_ROWS_CREATE) + await session.execute_many(SERVICE_ROWS_INSERT_PG, [(i, f"value_{i}") for i in range(100)]) + for i in range(iterations): + await session.fetch_one_or_none(SERVICE_ROWS_SELECT_BY_PAYLOAD_PG, (f"value_{i % 100}",)) + finally: + await config.close_pool() -async def sqlspec_oracle_lob_fetch_async_fetch_lobs_true_1k() -> None: - """Fetch 100 1 KiB Oracle CLOB rows with async sqlspec LOB locators enabled.""" - await _run_sqlspec_oracle_lob_fetch_async("1k", fetch_lobs=True) +def _get_psqlpy_pool_class() -> Any: + """Import psqlpy ConnectionPool lazily.""" + try: + from psqlpy import ConnectionPool + except ImportError: + return None + else: + return ConnectionPool -async def sqlspec_oracle_lob_fetch_async_fetch_lobs_true_100k() -> None: - """Fetch 100 100 KiB Oracle CLOB rows with async sqlspec LOB locators enabled.""" - await _run_sqlspec_oracle_lob_fetch_async("100k", fetch_lobs=True) +def _get_psqlpy_config() -> Any: + """Import PsqlpyConfig lazily.""" + try: + from sqlspec.adapters.psqlpy import PsqlpyConfig + except ImportError: + return None + else: + return PsqlpyConfig -def sqlspec_oracle_native_json_write() -> None: - """Write native Python mappings to an Oracle native JSON column.""" - _run_sqlspec_oracle_json_write(serialized=False) +async def _run_raw_psqlpy_rows(dsn: str) -> None: + pool_class = _get_psqlpy_pool_class() + if pool_class is None: + _benchmark_unavailable() + pool = pool_class(dsn=dsn, max_db_pool_size=max(2, POOL_SIZE)) + try: + conn = await pool.connection() + await conn.execute(SERVICE_ROWS_DROP) + await conn.execute(SERVICE_ROWS_CREATE) + await conn.execute_many(SERVICE_ROWS_INSERT_ASYNCPG, [[idx, val] for idx, val in _service_benchmark_rows()]) + res = await conn.fetch(SERVICE_ROWS_SELECT) + rows = res.result() + assert len(rows) == BENCHMARK_ROWS + finally: + pool.close() -def sqlspec_oracle_serialized_json_write() -> None: - """Write pre-serialized strings to an Oracle native JSON column.""" - _run_sqlspec_oracle_json_write(serialized=True) +async def _run_sqlspec_psqlpy_rows(dsn: str) -> None: + config_class = _get_psqlpy_config() + if config_class is None: + _benchmark_unavailable() + config = config_class(connection_config={"dsn": dsn, "max_db_pool_size": max(2, POOL_SIZE)}) + try: + spec = SQLSpec() + async with spec.provide_session(config) as session: + await session.execute(SERVICE_ROWS_DROP) + await session.execute(SERVICE_ROWS_CREATE) + await session.execute_many(SERVICE_ROWS_INSERT_ASYNCPG, _service_benchmark_rows()) + rows = await session.fetch(SERVICE_ROWS_SELECT) + assert len(rows) == BENCHMARK_ROWS + finally: + await config.close_pool() -def sqlspec_oracle_json_read() -> None: - """Read native JSON values from Oracle through SQLSpec.""" - _run_sqlspec_oracle_json_read() +async def _run_raw_psqlpy_hot_path(scenario: str) -> None: + pool_class = _get_psqlpy_pool_class() + if pool_class is None: + _benchmark_unavailable() + dsn = _postgres_benchmark_dsn() + iterations = _service_loop_iterations() + pool = pool_class(dsn=dsn, max_db_pool_size=max(2, POOL_SIZE)) + try: + conn = await pool.connection() + if scenario == "scalar_param_select": + for i in range(iterations): + res = await conn.fetch(SCALAR_PARAM_SELECT_ASYNCPG_SQL, [i]) + rows = res.result() + assert len(rows) == 1 + elif scenario == "no_param_select": + for _ in range(iterations): + res = await conn.fetch(NO_PARAM_SELECT_SQL) + rows = res.result() + assert len(rows) == 1 + elif scenario == "complex_query": + for ddl in POSTGRES_COMPLEX_DROP_DDL: + await conn.execute(ddl) + for ddl in POSTGRES_COMPLEX_SCHEMA_DDL: + await conn.execute(ddl) + for i in range(iterations): + res = await conn.fetch(POSTGRES_COMPLEX_QUERY_ASYNCPG_SQL, [i]) + res.result() + elif scenario in {"session_per_request", "observability_select"}: + await conn.execute(SERVICE_ROWS_DROP) + await conn.execute(SERVICE_ROWS_CREATE) + await conn.execute_many(SERVICE_ROWS_INSERT_ASYNCPG, [[i, f"value_{i}"] for i in range(100)]) + for i in range(iterations): + res = await conn.fetch(SERVICE_ROWS_SELECT_BY_PAYLOAD_ASYNCPG, [f"value_{i % 100}"]) + res.result() + finally: + pool.close() + + +async def _run_sqlspec_psqlpy_hot_path(scenario: str) -> None: + config_class = _get_psqlpy_config() + if config_class is None: + _benchmark_unavailable() + dsn = _postgres_benchmark_dsn() + iterations = _service_loop_iterations() + spec = SQLSpec() + obs_cfg = ( + ObservabilityConfig(statement_observers=(_noop_statement_observer,)) + if scenario == "observability_select" + else None + ) + config = config_class( + connection_config={"dsn": dsn, "max_db_pool_size": max(2, POOL_SIZE)}, observability_config=obs_cfg + ) + try: + if scenario == "scalar_param_select": + async with spec.provide_session(config) as session: + for i in range(iterations): + val = await session.select_value(SCALAR_PARAM_SELECT_ASYNCPG_SQL, i) + assert val == i + elif scenario == "no_param_select": + async with spec.provide_session(config) as session: + for _ in range(iterations): + res = await session.execute(NO_PARAM_SELECT_SQL) + assert res is not None + elif scenario == "complex_query": + async with spec.provide_session(config) as session: + for ddl in POSTGRES_COMPLEX_DROP_DDL: + await session.execute(ddl) + for ddl in POSTGRES_COMPLEX_SCHEMA_DDL: + await session.execute(ddl) + for i in range(iterations): + await session.execute(POSTGRES_COMPLEX_QUERY_ASYNCPG_SQL, i) + elif scenario == "session_per_request": + async with spec.provide_session(config) as session: + await session.execute(SERVICE_ROWS_DROP) + await session.execute(SERVICE_ROWS_CREATE) + await session.execute_many(SERVICE_ROWS_INSERT_ASYNCPG, [(i, f"value_{i}") for i in range(100)]) + for i in range(iterations): + async with spec.provide_session(config) as session: + await session.fetch_one_or_none(SERVICE_ROWS_SELECT_BY_PAYLOAD_ASYNCPG, (f"value_{i % 100}",)) + elif scenario == "observability_select": + async with spec.provide_session(config) as session: + await session.execute(SERVICE_ROWS_DROP) + await session.execute(SERVICE_ROWS_CREATE) + await session.execute_many(SERVICE_ROWS_INSERT_ASYNCPG, [(i, f"value_{i}") for i in range(100)]) + for i in range(iterations): + await session.fetch_one_or_none(SERVICE_ROWS_SELECT_BY_PAYLOAD_ASYNCPG, (f"value_{i % 100}",)) + finally: + await config.close_pool() + + +def _run_raw_oracle_hot_path(scenario: str) -> None: + oracledb = _get_oracledb() + if oracledb is None: + _benchmark_unavailable() + iterations = _service_loop_iterations() + conn = oracledb.connect(**_oracle_connect_config_from_env()) + try: + with conn.cursor() as cursor: + if scenario == "scalar_param_select": + for i in range(iterations): + cursor.execute(SCALAR_PARAM_SELECT_ORACLE_SQL, (i,)) + row = cursor.fetchone() + assert row is not None + assert row[0] == i + elif scenario == "no_param_select": + for _ in range(iterations): + cursor.execute(NO_PARAM_SELECT_ORACLE_SQL) + rows = cursor.fetchall() + assert len(rows) == 1 + elif scenario == "complex_query": + for tbl in ORACLE_COMPLEX_TABLES: + cursor.execute(_oracle_drop_table_sql(tbl)) + for ddl in ORACLE_COMPLEX_SCHEMA_DDL: + cursor.execute(ddl) + conn.commit() + for i in range(iterations): + cursor.execute(ORACLE_COMPLEX_QUERY_SQL, (i,)) + cursor.fetchall() + elif scenario in {"session_per_request", "observability_select"}: + cursor.execute(_oracle_drop_table_sql(ORACLE_KV_TABLE)) + cursor.execute(ORACLE_KV_CREATE_SQL) + cursor.executemany(ORACLE_KV_INSERT_SQL, [(f"value_{i}",) for i in range(100)]) + conn.commit() + for i in range(iterations): + cursor.execute(ORACLE_KV_SELECT_BY_VALUE_SQL, (f"value_{i % 100}",)) + cursor.fetchone() + finally: + conn.close() + + +def _run_sqlspec_oracle_hot_path(scenario: str) -> None: + from sqlspec.adapters.oracledb import OracleSyncConfig + + iterations = _service_loop_iterations() + spec = SQLSpec() + obs_cfg = ( + ObservabilityConfig(statement_observers=(_noop_statement_observer,)) + if scenario == "observability_select" + else None + ) + config = OracleSyncConfig(connection_config=_oracle_connection_config_from_env(), observability_config=obs_cfg) + try: + if scenario == "scalar_param_select": + with spec.provide_session(config) as session: + for i in range(iterations): + val = session.select_value(SCALAR_PARAM_SELECT_ORACLE_SQL, i) + assert val == i + elif scenario == "no_param_select": + with spec.provide_session(config) as session: + for _ in range(iterations): + res = session.execute(NO_PARAM_SELECT_ORACLE_SQL) + assert res is not None + elif scenario == "complex_query": + with spec.provide_session(config) as session: + for tbl in ORACLE_COMPLEX_TABLES: + session.execute_script(_oracle_drop_table_sql(tbl)) + for ddl in ORACLE_COMPLEX_SCHEMA_DDL: + session.execute(ddl) + for i in range(iterations): + session.execute(ORACLE_COMPLEX_QUERY_SQL, i) + elif scenario == "session_per_request": + with spec.provide_session(config) as session: + session.execute_script(_oracle_drop_table_sql(ORACLE_KV_TABLE)) + session.execute(ORACLE_KV_CREATE_SQL) + session.execute_many(ORACLE_KV_INSERT_SQL, [(f"value_{i}",) for i in range(100)]) + for i in range(iterations): + with spec.provide_session(config) as session: + session.fetch_one_or_none(ORACLE_KV_SELECT_BY_VALUE_SQL, (f"value_{i % 100}",)) + elif scenario == "observability_select": + with spec.provide_session(config) as session: + session.execute_script(_oracle_drop_table_sql(ORACLE_KV_TABLE)) + session.execute(ORACLE_KV_CREATE_SQL) + session.execute_many(ORACLE_KV_INSERT_SQL, [(f"value_{i}",) for i in range(100)]) + for i in range(iterations): + session.fetch_one_or_none(ORACLE_KV_SELECT_BY_VALUE_SQL, (f"value_{i % 100}",)) + _check_pool_leak(config.connection_instance, f"oracle/{scenario}") + config.close_pool() + finally: + if config.connection_instance is not None: + config.close_pool() + + +async def _run_sqlspec_oracle_async_hot_path(scenario: str) -> None: + from sqlspec.adapters.oracledb import OracleAsyncConfig + + iterations = _service_loop_iterations() + spec = SQLSpec() + obs_cfg = ( + ObservabilityConfig(statement_observers=(_noop_statement_observer,)) + if scenario == "observability_select" + else None + ) + config = OracleAsyncConfig(connection_config=_oracle_connection_config_from_env(), observability_config=obs_cfg) + try: + if scenario == "scalar_param_select": + async with spec.provide_session(config) as session: + for i in range(iterations): + val = await session.select_value(SCALAR_PARAM_SELECT_ORACLE_SQL, i) + assert val == i + elif scenario == "no_param_select": + async with spec.provide_session(config) as session: + for _ in range(iterations): + res = await session.execute(NO_PARAM_SELECT_ORACLE_SQL) + assert res is not None + elif scenario == "complex_query": + async with spec.provide_session(config) as session: + for tbl in ORACLE_COMPLEX_TABLES: + await session.execute_script(_oracle_drop_table_sql(tbl)) + for ddl in ORACLE_COMPLEX_SCHEMA_DDL: + await session.execute(ddl) + for i in range(iterations): + await session.execute(ORACLE_COMPLEX_QUERY_SQL, i) + elif scenario == "session_per_request": + async with spec.provide_session(config) as session: + await session.execute_script(_oracle_drop_table_sql(ORACLE_KV_TABLE)) + await session.execute(ORACLE_KV_CREATE_SQL) + await session.execute_many(ORACLE_KV_INSERT_SQL, [(f"value_{i}",) for i in range(100)]) + for i in range(iterations): + async with spec.provide_session(config) as session: + await session.fetch_one_or_none(ORACLE_KV_SELECT_BY_VALUE_SQL, (f"value_{i % 100}",)) + elif scenario == "observability_select": + async with spec.provide_session(config) as session: + await session.execute_script(_oracle_drop_table_sql(ORACLE_KV_TABLE)) + await session.execute(ORACLE_KV_CREATE_SQL) + await session.execute_many(ORACLE_KV_INSERT_SQL, [(f"value_{i}",) for i in range(100)]) + for i in range(iterations): + await session.fetch_one_or_none(ORACLE_KV_SELECT_BY_VALUE_SQL, (f"value_{i % 100}",)) + _check_pool_leak(config.connection_instance, f"oracle_async/{scenario}") + await config.close_pool() + finally: + if config.connection_instance is not None: + await config.close_pool() + +SQLSPEC_LABEL = "sqlspec (mypyc)" if _is_compiled() else "sqlspec" SCENARIO_REGISTRY: dict[tuple[str, str, str], Any] = { - # SQLite scenarios ("raw", "sqlite", "initialization"): raw_sqlite_initialization, ("raw", "sqlite", "write_heavy"): raw_sqlite_write_heavy, ("raw", "sqlite", "read_heavy"): raw_sqlite_read_heavy, @@ -2898,7 +4502,6 @@ def sqlspec_oracle_json_read() -> None: ("sqlalchemy", "sqlite", "read_heavy"): sqlalchemy_sqlite_read_heavy, ("sqlalchemy", "sqlite", "iterative_inserts"): sqlalchemy_sqlite_iterative_inserts, ("sqlalchemy", "sqlite", "repeated_queries"): sqlalchemy_sqlite_repeated_queries, - # DuckDB scenarios ("raw", "duckdb", "initialization"): raw_duckdb_initialization, ("raw", "duckdb", "write_heavy"): raw_duckdb_write_heavy, ("raw", "duckdb", "read_heavy"): raw_duckdb_read_heavy, @@ -2914,7 +4517,6 @@ def sqlspec_oracle_json_read() -> None: ("sqlalchemy", "duckdb", "read_heavy"): sqlalchemy_duckdb_read_heavy, ("sqlalchemy", "duckdb", "iterative_inserts"): sqlalchemy_duckdb_iterative_inserts, ("sqlalchemy", "duckdb", "repeated_queries"): sqlalchemy_duckdb_repeated_queries, - # Aiosqlite scenarios ("raw", "aiosqlite", "initialization"): raw_aiosqlite_initialization, ("raw", "aiosqlite", "write_heavy"): raw_aiosqlite_write_heavy, ("raw", "aiosqlite", "read_heavy"): raw_aiosqlite_read_heavy, @@ -2940,20 +4542,20 @@ def sqlspec_oracle_json_read() -> None: ("sqlspec", "adbc", "rows"): sqlspec_adbc_rows, ("raw", "duckdb", "bulk"): raw_duckdb_bulk, ("sqlspec", "duckdb", "bulk"): sqlspec_duckdb_bulk, - # PostgreSQL and CockroachDB service row scenarios ("raw", "psycopg_sync", "rows"): raw_psycopg_sync_rows, ("sqlspec", "psycopg_sync", "rows"): sqlspec_psycopg_sync_rows, ("raw", "psycopg_async", "rows"): raw_psycopg_async_rows, ("sqlspec", "psycopg_async", "rows"): sqlspec_psycopg_async_rows, ("raw", "asyncpg", "rows"): raw_asyncpg_rows, ("sqlspec", "asyncpg", "rows"): sqlspec_asyncpg_rows, + ("raw", "psqlpy", "rows"): raw_psqlpy_rows, + ("sqlspec", "psqlpy", "rows"): sqlspec_psqlpy_rows, ("raw", "cockroach_psycopg_sync", "rows"): raw_cockroach_psycopg_sync_rows, ("sqlspec", "cockroach_psycopg_sync", "rows"): sqlspec_cockroach_psycopg_sync_rows, ("raw", "cockroach_psycopg_async", "rows"): raw_cockroach_psycopg_async_rows, ("sqlspec", "cockroach_psycopg_async", "rows"): sqlspec_cockroach_psycopg_async_rows, ("raw", "cockroach_asyncpg", "rows"): raw_cockroach_asyncpg_rows, ("sqlspec", "cockroach_asyncpg", "rows"): sqlspec_cockroach_asyncpg_rows, - # Asyncpg scenarios ("raw", "asyncpg", "initialization"): raw_asyncpg_initialization, ("raw", "asyncpg", "write_heavy"): raw_asyncpg_write_heavy, ("raw", "asyncpg", "read_heavy"): raw_asyncpg_read_heavy, @@ -2969,7 +4571,6 @@ def sqlspec_oracle_json_read() -> None: ("sqlalchemy", "asyncpg", "read_heavy"): sqlalchemy_asyncpg_read_heavy, ("sqlalchemy", "asyncpg", "iterative_inserts"): sqlalchemy_asyncpg_iterative_inserts, ("sqlalchemy", "asyncpg", "repeated_queries"): sqlalchemy_asyncpg_repeated_queries, - # Extended SQLite scenarios (raw vs sqlspec only) ("raw", "sqlite", "dict_key_transform"): raw_sqlite_dict_key_transform, ("sqlspec", "sqlite", "dict_key_transform"): sqlspec_sqlite_dict_key_transform, ("raw", "sqlite", "schema_mapping"): raw_sqlite_schema_mapping, @@ -2980,7 +4581,76 @@ def sqlspec_oracle_json_read() -> None: ("sqlspec", "sqlite", "complex_parameters"): sqlspec_sqlite_complex_parameters, ("raw", "sqlite", "thin_path_stress"): raw_sqlite_thin_path_stress, ("sqlspec", "sqlite", "thin_path_stress"): sqlspec_sqlite_thin_path_stress, - # Extended Oracle LOB fetch scenarios + ("raw", "sqlite", "scalar_param_select"): raw_sqlite_scalar_param_select, + ("sqlspec", "sqlite", "scalar_param_select"): sqlspec_sqlite_scalar_param_select, + ("raw", "sqlite", "no_param_select"): raw_sqlite_no_param_select, + ("sqlspec", "sqlite", "no_param_select"): sqlspec_sqlite_no_param_select, + ("raw", "sqlite", "complex_query"): raw_sqlite_complex_query, + ("sqlspec", "sqlite", "complex_query"): sqlspec_sqlite_complex_query, + ("raw", "sqlite", "session_per_request"): raw_sqlite_session_per_request, + ("sqlspec", "sqlite", "session_per_request"): sqlspec_sqlite_session_per_request, + ("raw", "sqlite", "observability_select"): raw_sqlite_observability_select, + ("sqlspec", "sqlite", "observability_select"): sqlspec_sqlite_observability_select, + ("raw", "aiosqlite", "scalar_param_select"): raw_aiosqlite_scalar_param_select, + ("sqlspec", "aiosqlite", "scalar_param_select"): sqlspec_aiosqlite_scalar_param_select, + ("raw", "aiosqlite", "no_param_select"): raw_aiosqlite_no_param_select, + ("sqlspec", "aiosqlite", "no_param_select"): sqlspec_aiosqlite_no_param_select, + ("raw", "aiosqlite", "complex_query"): raw_aiosqlite_complex_query, + ("sqlspec", "aiosqlite", "complex_query"): sqlspec_aiosqlite_complex_query, + ("raw", "aiosqlite", "session_per_request"): raw_aiosqlite_session_per_request, + ("sqlspec", "aiosqlite", "session_per_request"): sqlspec_aiosqlite_session_per_request, + ("raw", "aiosqlite", "observability_select"): raw_aiosqlite_observability_select, + ("sqlspec", "aiosqlite", "observability_select"): sqlspec_aiosqlite_observability_select, + ("raw", "duckdb", "scalar_param_select"): raw_duckdb_scalar_param_select, + ("sqlspec", "duckdb", "scalar_param_select"): sqlspec_duckdb_scalar_param_select, + ("raw", "duckdb", "no_param_select"): raw_duckdb_no_param_select, + ("sqlspec", "duckdb", "no_param_select"): sqlspec_duckdb_no_param_select, + ("raw", "duckdb", "complex_query"): raw_duckdb_complex_query, + ("sqlspec", "duckdb", "complex_query"): sqlspec_duckdb_complex_query, + ("raw", "duckdb", "session_per_request"): raw_duckdb_session_per_request, + ("sqlspec", "duckdb", "session_per_request"): sqlspec_duckdb_session_per_request, + ("raw", "duckdb", "observability_select"): raw_duckdb_observability_select, + ("sqlspec", "duckdb", "observability_select"): sqlspec_duckdb_observability_select, + ("raw", "asyncpg", "scalar_param_select"): raw_asyncpg_scalar_param_select, + ("sqlspec", "asyncpg", "scalar_param_select"): sqlspec_asyncpg_scalar_param_select, + ("raw", "asyncpg", "no_param_select"): raw_asyncpg_no_param_select, + ("sqlspec", "asyncpg", "no_param_select"): sqlspec_asyncpg_no_param_select, + ("raw", "asyncpg", "complex_query"): raw_asyncpg_complex_query, + ("sqlspec", "asyncpg", "complex_query"): sqlspec_asyncpg_complex_query, + ("raw", "asyncpg", "session_per_request"): raw_asyncpg_session_per_request, + ("sqlspec", "asyncpg", "session_per_request"): sqlspec_asyncpg_session_per_request, + ("raw", "asyncpg", "observability_select"): raw_asyncpg_observability_select, + ("sqlspec", "asyncpg", "observability_select"): sqlspec_asyncpg_observability_select, + ("raw", "psycopg_sync", "scalar_param_select"): raw_psycopg_sync_scalar_param_select, + ("sqlspec", "psycopg_sync", "scalar_param_select"): sqlspec_psycopg_sync_scalar_param_select, + ("raw", "psycopg_sync", "no_param_select"): raw_psycopg_sync_no_param_select, + ("sqlspec", "psycopg_sync", "no_param_select"): sqlspec_psycopg_sync_no_param_select, + ("raw", "psycopg_sync", "complex_query"): raw_psycopg_sync_complex_query, + ("sqlspec", "psycopg_sync", "complex_query"): sqlspec_psycopg_sync_complex_query, + ("raw", "psycopg_sync", "session_per_request"): raw_psycopg_sync_session_per_request, + ("sqlspec", "psycopg_sync", "session_per_request"): sqlspec_psycopg_sync_session_per_request, + ("raw", "psycopg_sync", "observability_select"): raw_psycopg_sync_observability_select, + ("sqlspec", "psycopg_sync", "observability_select"): sqlspec_psycopg_sync_observability_select, + ("raw", "psycopg_async", "scalar_param_select"): raw_psycopg_async_scalar_param_select, + ("sqlspec", "psycopg_async", "scalar_param_select"): sqlspec_psycopg_async_scalar_param_select, + ("raw", "psycopg_async", "no_param_select"): raw_psycopg_async_no_param_select, + ("sqlspec", "psycopg_async", "no_param_select"): sqlspec_psycopg_async_no_param_select, + ("raw", "psycopg_async", "complex_query"): raw_psycopg_async_complex_query, + ("sqlspec", "psycopg_async", "complex_query"): sqlspec_psycopg_async_complex_query, + ("raw", "psycopg_async", "session_per_request"): raw_psycopg_async_session_per_request, + ("sqlspec", "psycopg_async", "session_per_request"): sqlspec_psycopg_async_session_per_request, + ("raw", "psycopg_async", "observability_select"): raw_psycopg_async_observability_select, + ("sqlspec", "psycopg_async", "observability_select"): sqlspec_psycopg_async_observability_select, + ("raw", "psqlpy", "scalar_param_select"): raw_psqlpy_scalar_param_select, + ("sqlspec", "psqlpy", "scalar_param_select"): sqlspec_psqlpy_scalar_param_select, + ("raw", "psqlpy", "no_param_select"): raw_psqlpy_no_param_select, + ("sqlspec", "psqlpy", "no_param_select"): sqlspec_psqlpy_no_param_select, + ("raw", "psqlpy", "complex_query"): raw_psqlpy_complex_query, + ("sqlspec", "psqlpy", "complex_query"): sqlspec_psqlpy_complex_query, + ("raw", "psqlpy", "session_per_request"): raw_psqlpy_session_per_request, + ("sqlspec", "psqlpy", "session_per_request"): sqlspec_psqlpy_session_per_request, + ("raw", "psqlpy", "observability_select"): raw_psqlpy_observability_select, + ("sqlspec", "psqlpy", "observability_select"): sqlspec_psqlpy_observability_select, ("raw", "oracle", "lob_fetch_1k"): raw_oracle_lob_fetch_1k, ("raw", "oracle", "lob_fetch_100k"): raw_oracle_lob_fetch_100k, ("sqlspec", "oracle", "lob_fetch_1k"): sqlspec_oracle_lob_fetch_1k, @@ -2994,48 +4664,22 @@ def sqlspec_oracle_json_read() -> None: ("sqlspec_native_json", "oracle", "json_write"): sqlspec_oracle_native_json_write, ("sqlspec_serialized_json", "oracle", "json_write"): sqlspec_oracle_serialized_json_write, ("sqlspec", "oracle", "json_read"): sqlspec_oracle_json_read, + ("raw", "oracle", "scalar_param_select"): raw_oracle_scalar_param_select, + ("sqlspec", "oracle", "scalar_param_select"): sqlspec_oracle_scalar_param_select, + ("sqlspec_async", "oracle", "scalar_param_select"): sqlspec_oracle_async_scalar_param_select, + ("raw", "oracle", "no_param_select"): raw_oracle_no_param_select, + ("sqlspec", "oracle", "no_param_select"): sqlspec_oracle_no_param_select, + ("sqlspec_async", "oracle", "no_param_select"): sqlspec_oracle_async_no_param_select, + ("raw", "oracle", "complex_query"): raw_oracle_complex_query, + ("sqlspec", "oracle", "complex_query"): sqlspec_oracle_complex_query, + ("sqlspec_async", "oracle", "complex_query"): sqlspec_oracle_async_complex_query, + ("raw", "oracle", "session_per_request"): raw_oracle_session_per_request, + ("sqlspec", "oracle", "session_per_request"): sqlspec_oracle_session_per_request, + ("sqlspec_async", "oracle", "session_per_request"): sqlspec_oracle_async_session_per_request, + ("raw", "oracle", "observability_select"): raw_oracle_observability_select, + ("sqlspec", "oracle", "observability_select"): sqlspec_oracle_observability_select, + ("sqlspec_async", "oracle", "observability_select"): sqlspec_oracle_async_observability_select, } - -def print_benchmark_table(results: list[dict[str, Any]]) -> None: - console = Console() - table = Table(title="Benchmark Results") - table.add_column("Driver", style="cyan", no_wrap=True) - table.add_column("Library", style="magenta") - table.add_column("Scenario", style="green") - table.add_column("Time (s)", justify="right", style="yellow") - table.add_column("Stddev", justify="right") - table.add_column("IQR", justify="right") - table.add_column("Noise", justify="center") - table.add_column("% Slower vs Raw", justify="right", style="red") - - # Check if any result has multiple iterations - multi_iter = any(len(row.get("times", [])) > 1 for row in results) - - # Build a lookup for raw times: {(driver, scenario): time} - raw_times: dict[tuple[str, str], float] = {} - for row in results: - if row["library"] == "raw": - raw_times[row["driver"], row["scenario"]] = row["time"] - - for row in results: - driver = row["driver"] - scenario = row["scenario"] - lib = row["library"] - t = row["time"] - times = row.get("times", [t]) - stddev = float(row.get("stddev", 0.0)) - iqr = float(row.get("iqr", 0.0)) - noise = "yes" if row.get("noisy", False) else "" - if lib == "raw": - percent_slower = "---" - else: - raw_time = raw_times.get((driver, scenario)) - percent_slower = f"{100 * (t - raw_time) / raw_time:.1f}%" if raw_time and raw_time > 0 else "n/a" - time_str = f"{t:.4f} ({min(times):.4f}-{max(times):.4f})" if multi_iter and len(times) > 1 else f"{t:.4f}" - table.add_row(driver, lib, scenario, time_str, f"{stddev:.4f}", f"{iqr:.4f}", noise, percent_slower) - console.print(table) - - if __name__ == "__main__": main() diff --git a/tools/scripts/bench_compare.py b/tools/scripts/bench_compare.py index ef0d5a83b..50edaad36 100644 --- a/tools/scripts/bench_compare.py +++ b/tools/scripts/bench_compare.py @@ -2,7 +2,7 @@ Accepts two JSON result files (baseline and current) produced by ``bench.py --json-output`` and produces a rich table showing per-scenario -comparison with absolute and percentage changes. +comparison with absolute or relative ratio changes and metadata parity checks. Run with:: @@ -20,102 +20,202 @@ BenchmarkKey = tuple[str, str, str] -__all__ = ("main", "print_comparison_table") +HARD_METADATA_KEYS: tuple[str, ...] = ("rows", "pool_size", "mypyc_compiled") +SOFT_METADATA_KEYS: tuple[str, ...] = ("iterations", "warmup", "environment") + +__all__ = ( + "HARD_METADATA_KEYS", + "SOFT_METADATA_KEYS", + "compute_comparison_rows", + "format_run_args", + "main", + "print_comparison_table", + "validate_metadata_parity", +) -def _load_results(path: str) -> dict[str, Any]: - """Load and validate a benchmark JSON file. +def validate_metadata_parity(baseline: dict[str, Any], current: dict[str, Any]) -> tuple[list[str], list[str]]: + """Compare run metadata between baseline and current benchmark payloads. Args: - path: Path to the JSON file. + baseline: Parsed baseline benchmark JSON payload. + current: Parsed current benchmark JSON payload. Returns: - Parsed JSON data. - - Raises: - click.ClickException: If the file cannot be loaded or is invalid. + Tuple of (hard_mismatches, soft_warnings). """ - try: - with Path(path).open() as f: - data = json.load(f) - except (OSError, json.JSONDecodeError) as exc: - msg = f"Failed to load {path}: {exc}" - raise click.ClickException(msg) from exc - - if "results" not in data: - msg = f"Invalid benchmark file {path}: missing 'results' key" - raise click.ClickException(msg) - - return cast("dict[str, Any]", data) - - -def _build_lookup(data: dict[str, Any]) -> dict[BenchmarkKey, dict[str, Any]]: - """Build a lookup table from benchmark results. + base_meta = baseline.get("metadata", {}) + curr_meta = current.get("metadata", {}) + hard_mismatches = [ + f"{key}: baseline={base_meta[key]!r} != current={curr_meta[key]!r}" + for key in HARD_METADATA_KEYS + if key in base_meta and key in curr_meta and base_meta[key] != curr_meta[key] + ] + soft_warnings = [ + f"{key}: baseline={base_meta[key]!r} != current={curr_meta[key]!r}" + for key in SOFT_METADATA_KEYS + if key in base_meta and key in curr_meta and base_meta[key] != curr_meta[key] + ] + return hard_mismatches, soft_warnings + + +def format_run_args(baseline: dict[str, Any]) -> str: + """Build bench.py CLI flags matching a baseline JSON file's metadata. Args: - data: Parsed benchmark JSON data. + baseline: Parsed baseline benchmark JSON payload. Returns: - Dictionary mapping (driver, library, scenario) to result payload. + Space-joined CLI argument string for ``bench.py``. """ - lookup: dict[BenchmarkKey, dict[str, Any]] = {} - for result in data["results"]: - key = (result["driver"], result.get("library_key", result["library"]), result["scenario"]) - lookup[key] = result - return lookup - - -def _missing_required_scenarios(data: dict[str, Any], required: set[BenchmarkKey]) -> set[BenchmarkKey]: - """Return required benchmark identities absent from a result payload.""" - return required - set(_build_lookup(data)) - - -def _format_time(result: dict[str, Any]) -> str: - time_value = float(result["time"]) - stddev = result.get("stddev") - if isinstance(stddev, int | float): - return f"{time_value:.4f} +/- {float(stddev):.4f}" - return f"{time_value:.4f}" - - -def _status_text(change_pct: float, threshold: float = 5.0) -> tuple[str, str]: - """Determine status text and style based on percentage change. + meta = baseline.get("metadata", {}) + args: list[str] = [] + if "rows" in meta: + args.extend(("--rows", str(meta["rows"]))) + if "iterations" in meta: + args.extend(("--iterations", str(meta["iterations"]))) + if "pool_size" in meta: + args.extend(("--pool-size", str(meta["pool_size"]))) + if "warmup" in meta: + args.extend(("--warmup", str(meta["warmup"]))) + return " ".join(args) + + +def compute_comparison_rows( + baseline: dict[str, Any], current: dict[str, Any], *, mode: str = "absolute", threshold: float = 5.0 +) -> list[dict[str, Any]]: + """Compute per-scenario comparison rows in absolute or ratio mode. Args: - change_pct: Percentage change (positive = slower, negative = faster). - threshold: Percentage threshold for considering a change significant. + baseline: Parsed baseline benchmark JSON data. + current: Parsed current benchmark JSON data. + mode: Comparison mode (``"absolute"`` or ``"ratio"``). + threshold: Percentage threshold for flagging changes as significant. Returns: - Tuple of (status_text, rich_style). + List of comparison row dictionaries ordered by (driver, library_key, scenario). """ - if change_pct < -threshold: - return "IMPROVED", "bold green" - if change_pct > threshold: - return "REGRESSED", "bold red" - return "unchanged", "dim" + baseline_lookup = _build_lookup(baseline) + current_lookup = _build_lookup(current) + all_keys = sorted(set(baseline_lookup.keys()) | set(current_lookup.keys())) + + rows: list[dict[str, Any]] = [] + for key in all_keys: + driver, library_key, scenario = key + baseline_result = baseline_lookup.get(key) + current_result = current_lookup.get(key) + library_label = (current_result or baseline_result or {}).get("library", library_key) + if baseline_result is None: + assert current_result is not None + rows.append({ + "driver": driver, + "library_key": library_key, + "library_label": library_label, + "scenario": scenario, + "baseline_result": None, + "current_result": current_result, + "metric_kind": "new", + "baseline_metric": None, + "current_metric": float(current_result["time"]), + "abs_change": None, + "pct_change": None, + "status_text": "NEW", + "status_style": "dim", + }) + continue + + if current_result is None: + rows.append({ + "driver": driver, + "library_key": library_key, + "library_label": library_label, + "scenario": scenario, + "baseline_result": baseline_result, + "current_result": None, + "metric_kind": "removed", + "baseline_metric": float(baseline_result["time"]), + "current_metric": None, + "abs_change": None, + "pct_change": None, + "status_text": "REMOVED", + "status_style": "dim", + }) + continue -def print_comparison_table(baseline: dict[str, Any], current: dict[str, Any], *, threshold: float = 5.0) -> None: + baseline_time = float(baseline_result["time"]) + current_time = float(current_result["time"]) + abs_change = current_time - baseline_time + + metric_kind = "absolute" + baseline_metric = baseline_time + current_metric = current_time + + if mode == "ratio": + raw_key = (driver, "raw", scenario) + base_raw = baseline_lookup.get(raw_key) + curr_raw = current_lookup.get(raw_key) + if library_key == "raw": + metric_kind = "raw-ref" + elif ( + base_raw is not None + and curr_raw is not None + and float(base_raw["time"]) > 0 + and float(curr_raw["time"]) > 0 + ): + metric_kind = "ratio" + baseline_metric = baseline_time / float(base_raw["time"]) + current_metric = current_time / float(curr_raw["time"]) + else: + metric_kind = "abs-fallback" + + if metric_kind == "raw-ref": + pct_change = ((current_time - baseline_time) / baseline_time * 100.0) if baseline_time > 0 else 0.0 + status_text, status_style = "unchanged", "dim" + else: + delta_metric = current_metric - baseline_metric + pct_change = (delta_metric / baseline_metric * 100.0) if baseline_metric > 0 else 0.0 + status_text, status_style = _status_text(pct_change, threshold) + + rows.append({ + "driver": driver, + "library_key": library_key, + "library_label": library_label, + "scenario": scenario, + "baseline_result": baseline_result, + "current_result": current_result, + "metric_kind": metric_kind, + "baseline_metric": baseline_metric, + "current_metric": current_metric, + "abs_change": abs_change, + "pct_change": pct_change, + "status_text": status_text, + "status_style": status_style, + }) + + return rows + + +def print_comparison_table( + baseline: dict[str, Any], current: dict[str, Any], *, threshold: float = 5.0, mode: str = "absolute" +) -> None: """Print a rich comparison table between two benchmark runs. Args: baseline: Parsed baseline benchmark JSON data. current: Parsed current benchmark JSON data. threshold: Percentage threshold for flagging changes as significant. + mode: Comparison mode (``"absolute"`` or ``"ratio"``). """ console = Console() + rows = compute_comparison_rows(baseline, current, mode=mode, threshold=threshold) - baseline_lookup = _build_lookup(baseline) - current_lookup = _build_lookup(current) - - # Collect all keys from both runs - all_keys = sorted(set(baseline_lookup.keys()) | set(current_lookup.keys())) - - if not all_keys: + if not rows: console.print("[yellow]No benchmark results to compare.[/yellow]") return - table = Table(title="Benchmark Comparison: Baseline vs Current") + title_suffix = " (Ratio Mode)" if mode == "ratio" else "" + table = Table(title=f"Benchmark Comparison: Baseline vs Current{title_suffix}") table.add_column("Driver", style="cyan", no_wrap=True) table.add_column("Library", style="magenta") table.add_column("Scenario", style="green") @@ -129,20 +229,22 @@ def print_comparison_table(baseline: dict[str, Any], current: dict[str, Any], *, regressed_count = 0 unchanged_count = 0 - for key in all_keys: - driver, library, scenario = key - baseline_result = baseline_lookup.get(key) - current_result = current_lookup.get(key) - library_label = (current_result or baseline_result or {}).get("library", library) + for row in rows: + driver = row["driver"] + library_label = row["library_label"] + scenario = row["scenario"] + baseline_result = row["baseline_result"] + current_result = row["current_result"] + status_text = row["status_text"] + status_style = row["status_style"] - if baseline_result is None: - assert current_result is not None + if baseline_result is None and current_result is not None: table.add_row( driver, library_label, scenario, "n/a", _format_time(current_result), "n/a", "n/a", "[dim]NEW[/dim]" ) continue - if current_result is None: + if current_result is None and baseline_result is not None: table.add_row( driver, library_label, @@ -155,12 +257,8 @@ def print_comparison_table(baseline: dict[str, Any], current: dict[str, Any], *, ) continue - baseline_time = float(baseline_result["time"]) - current_time = float(current_result["time"]) - abs_change = current_time - baseline_time - pct_change = abs_change / baseline_time * 100.0 if baseline_time > 0 else 0.0 - - status_text, status_style = _status_text(pct_change, threshold) + abs_change = float(row["abs_change"]) + pct_change = float(row["pct_change"]) if status_text == "IMPROVED": improved_count += 1 @@ -169,8 +267,7 @@ def print_comparison_table(baseline: dict[str, Any], current: dict[str, Any], *, else: unchanged_count += 1 - # Color the change values - change_style = "green" if abs_change < 0 else ("red" if abs_change > 0 else "dim") + change_style = "green" if pct_change < 0 else ("red" if pct_change > 0 else "dim") table.add_row( driver, @@ -185,7 +282,6 @@ def print_comparison_table(baseline: dict[str, Any], current: dict[str, Any], *, console.print(table) - # Print summary console.print() total = improved_count + regressed_count + unchanged_count console.print(f" Total scenarios compared: {total}") @@ -193,7 +289,6 @@ def print_comparison_table(baseline: dict[str, Any], current: dict[str, Any], *, console.print(f" [red]Regressed: {regressed_count}[/red]") console.print(f" [dim]Unchanged: {unchanged_count}[/dim]") - # Print metadata comparison baseline_meta = baseline.get("metadata", {}) current_meta = current.get("metadata", {}) if baseline_meta or current_meta: @@ -215,12 +310,30 @@ def print_comparison_table(baseline: dict[str, Any], current: dict[str, Any], *, @click.command() @click.argument("baseline", type=click.Path(exists=True)) -@click.argument("current", type=click.Path(exists=True)) +@click.argument("current", required=False, type=click.Path(exists=True)) @click.option( "--threshold", default=5.0, show_default=True, help="Percentage threshold for flagging changes as significant" ) -def main(baseline: str, current: str, threshold: float) -> None: - """Compare two benchmark result JSON files. +@click.option( + "--mode", + type=click.Choice(["absolute", "ratio"]), + default="absolute", + show_default=True, + help="Comparison mode: absolute seconds or relative sqlspec/raw ratio", +) +@click.option( + "--allow-metadata-mismatch", + is_flag=True, + default=False, + help="Proceed even when hard metadata keys (rows, pool_size, mypyc_compiled) differ", +) +@click.option( + "--print-run-args", is_flag=True, default=False, help="Print bench.py CLI flags matching BASELINE metadata and exit" +) +def main( + baseline: str, current: str | None, threshold: float, mode: str, allow_metadata_mismatch: bool, print_run_args: bool +) -> None: + """Compare two benchmark result JSON files or print baseline run args. BASELINE is the path to the baseline benchmark JSON file. CURRENT is the path to the current benchmark JSON file. @@ -228,30 +341,114 @@ def main(baseline: str, current: str, threshold: float) -> None: Both files should be produced by ``bench.py --json-output ``. """ baseline_data = _load_results(baseline) + if print_run_args: + click.echo(format_run_args(baseline_data)) + return + + if current is None: + raise click.ClickException("Missing argument 'CURRENT'.") + current_data = _load_results(current) + hard_mismatches, soft_warnings = validate_metadata_parity(baseline_data, current_data) + + for warning in soft_warnings: + click.secho(f"Metadata warning: {warning}", fg="yellow") - print_comparison_table(baseline_data, current_data, threshold=threshold) + if hard_mismatches: + for mismatch in hard_mismatches: + click.secho(f"Metadata mismatch: {mismatch}", fg="red") + if not allow_metadata_mismatch: + sys.exit(2) + + print_comparison_table(baseline_data, current_data, threshold=threshold, mode=mode) - # Exit with non-zero if any regressions detected - current_lookup = _build_lookup(current_data) baseline_lookup = _build_lookup(baseline_data) missing = _missing_required_scenarios(current_data, set(baseline_lookup)) if missing: missing_text = ", ".join("/".join(key) for key in sorted(missing)) click.secho(f"\nMissing required benchmark scenario(s): {missing_text}", fg="red") sys.exit(1) - regressions = 0 - for key in set(baseline_lookup.keys()) & set(current_lookup.keys()): - bt = float(baseline_lookup[key]["time"]) - ct = float(current_lookup[key]["time"]) - if bt > 0 and ((ct - bt) / bt) * 100.0 > threshold: - regressions += 1 + + rows = compute_comparison_rows(baseline_data, current_data, mode=mode, threshold=threshold) + regressions = sum(1 for row in rows if row["status_text"] == "REGRESSED") if regressions > 0: click.secho(f"\n{regressions} regression(s) detected (>{threshold}% slower).", fg="red") sys.exit(1) - else: - click.secho("\nNo regressions detected.", fg="green") + click.secho("\nNo regressions detected.", fg="green") + + +def _load_results(path: str) -> dict[str, Any]: + """Load and validate a benchmark JSON file. + + Args: + path: Path to the JSON file. + + Returns: + Parsed JSON data. + + Raises: + click.ClickException: If the file cannot be loaded or is invalid. + """ + try: + with Path(path).open() as f: + data = json.load(f) + except (OSError, json.JSONDecodeError) as exc: + msg = f"Failed to load {path}: {exc}" + raise click.ClickException(msg) from exc + + if "results" not in data: + msg = f"Invalid benchmark file {path}: missing 'results' key" + raise click.ClickException(msg) + + return cast("dict[str, Any]", data) + + +def _build_lookup(data: dict[str, Any]) -> dict[BenchmarkKey, dict[str, Any]]: + """Build a lookup table from benchmark results. + + Args: + data: Parsed benchmark JSON data. + + Returns: + Dictionary mapping (driver, library, scenario) to result payload. + """ + lookup: dict[BenchmarkKey, dict[str, Any]] = {} + for result in data["results"]: + key = (result["driver"], result.get("library_key", result["library"]), result["scenario"]) + lookup[key] = result + return lookup + + +def _missing_required_scenarios(data: dict[str, Any], required: set[BenchmarkKey]) -> set[BenchmarkKey]: + """Return required benchmark identities absent from a result payload.""" + return required - set(_build_lookup(data)) + + +def _format_time(result: dict[str, Any]) -> str: + """Format a benchmark result's time and optional stddev.""" + time_value = float(result["time"]) + stddev = result.get("stddev") + if isinstance(stddev, int | float): + return f"{time_value:.4f} +/- {float(stddev):.4f}" + return f"{time_value:.4f}" + + +def _status_text(change_pct: float, threshold: float = 5.0) -> tuple[str, str]: + """Determine status text and style based on percentage change. + + Args: + change_pct: Percentage change (positive = slower, negative = faster). + threshold: Percentage threshold for considering a change significant. + + Returns: + Tuple of (status_text, rich_style). + """ + if change_pct < -threshold: + return "IMPROVED", "bold green" + if change_pct > threshold: + return "REGRESSED", "bold red" + return "unchanged", "dim" if __name__ == "__main__": diff --git a/tools/scripts/bench_gate.py b/tools/scripts/bench_gate.py index bcc4de21b..051d590e5 100644 --- a/tools/scripts/bench_gate.py +++ b/tools/scripts/bench_gate.py @@ -14,13 +14,14 @@ uv run python tools/scripts/bench_gate.py uv run python tools/scripts/bench_gate.py --rows 5000 --iterations 5 - uv run python tools/scripts/bench_gate.py --threshold-iterative 50 # stricter + uv run python tools/scripts/bench_gate.py --threshold-iterative 50 """ import importlib.util import json import sys import time +from contextlib import suppress from pathlib import Path from typing import Any, cast @@ -39,8 +40,6 @@ "run_gate", ) - -# Import bench.py from the same directory, regardless of working directory _bench_path = Path(__file__).parent / "bench.py" _spec = importlib.util.spec_from_file_location("bench", _bench_path) assert _spec is not None @@ -48,10 +47,6 @@ bench_mod = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(bench_mod) -# --------------------------------------------------------------------------- -# Default overhead thresholds (percentage of raw time) -# --------------------------------------------------------------------------- - DEFAULT_THRESHOLDS: dict[str, float] = { "iterative_inserts": 100.0, "repeated_queries": 5.0, @@ -59,11 +54,8 @@ "read_heavy": 12.0, } -# Core scenarios to gate on GATE_SCENARIOS = ["iterative_inserts", "repeated_queries", "write_heavy", "read_heavy"] -# PRD benchmark matrix for mypyc expansion work. This keeps the benchmark -# expectations next to the scripts that actually exercise them. BENCHMARK_SCENARIO_MATRIX: dict[str, dict[str, str | tuple[str, ...]]] = { "parameter_pipeline": { "tracked_by": "tools/scripts/bench_subsystems.py + tools/scripts/bench.py", @@ -138,20 +130,16 @@ def run_gate( Returns: Tuple of (results list, all_passed boolean). """ - # Set bench.py global rows cast("Any", bench_mod).ROWS_TO_INSERT = rows - # Run benchmarks using the same infrastructure as bench.py errors: list[str] = [] bench_results = bench_mod.run_benchmark("sqlite", errors, iterations=iterations, warmup=warmup) - # Build lookup: (library, scenario) -> median time time_lookup: dict[tuple[str, str], float] = {} for result in bench_results: key = (result["library"], result["scenario"]) time_lookup[key] = result["time"] - # Determine sqlspec label (may include "(mypyc)") sqlspec_label = bench_mod.SQLSPEC_LABEL gate_results: list[dict[str, Any]] = [] @@ -240,32 +228,6 @@ def print_gate_table(results: list[dict[str, Any]]) -> None: console.print(table) -def _write_json_results( - results: list[dict[str, Any]], - output_path: str | Path, - *, - rows: int, - iterations: int, - warmup: int, - thresholds: dict[str, float], - all_passed: bool, -) -> None: - """Write a performance gate report to JSON.""" - output = { - "metadata": { - "rows": rows, - "iterations": iterations, - "warmup": warmup, - "mypyc_compiled": bench_mod._is_compiled(), - "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z"), - }, - "thresholds": thresholds, - "all_passed": all_passed, - "results": results, - } - Path(output_path).write_text(json.dumps(output, indent=2, sort_keys=True)) - - @click.command() @click.option("--rows", default=10_000, show_default=True, help="Number of rows per scenario") @click.option( @@ -281,6 +243,12 @@ def _write_json_results( help="Number of warmup iterations (not timed)", ) @click.option("--json-output", default=None, type=click.Path(), help="Write gate results to a JSON file") +@click.option( + "--include-subsystems/--no-include-subsystems", + default=False, + show_default=True, + help="Also run and display subsystem and stub-driver micro-benchmarks (report-only)", +) @click.option( "--threshold-iterative", "threshold_iterative", @@ -313,6 +281,7 @@ def main( rows: int, iterations: int, warmup: int, + include_subsystems: bool, threshold_iterative: float, threshold_repeated: float, threshold_write: float, @@ -341,6 +310,9 @@ def main( results, all_passed = run_gate(rows=rows, iterations=iterations, warmup=warmup, thresholds=thresholds) print_gate_table(results) + if include_subsystems: + _run_report_only_subsystems(iterations=max(100, rows // 10), warmup=max(5, warmup)) + if json_output is not None: _write_json_results( results, @@ -362,5 +334,46 @@ def main( sys.exit(1) +def _run_report_only_subsystems(*, iterations: int, warmup: int) -> None: + """Run subsystem micro-benchmarks in report-only mode.""" + from tools.scripts.bench_subsystems import _make_temp_db, _setup_test_table, print_results_table, run_benchmarks + + db_path = _make_temp_db() + try: + _setup_test_table(db_path) + click.echo("\nRunning report-only subsystem micro-benchmarks...") + sub_results = run_benchmarks(db_path, iterations=iterations, warmup=warmup, profile=False) + print_results_table(sub_results) + finally: + with suppress(OSError): + db_path.unlink() + + +def _write_json_results( + results: list[dict[str, Any]], + output_path: str | Path, + *, + rows: int, + iterations: int, + warmup: int, + thresholds: dict[str, float], + all_passed: bool, +) -> None: + """Write a performance gate report to JSON.""" + output = { + "metadata": { + "rows": rows, + "iterations": iterations, + "warmup": warmup, + "mypyc_compiled": bench_mod._is_compiled(), + "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z"), + }, + "thresholds": thresholds, + "all_passed": all_passed, + "results": results, + } + Path(output_path).write_text(json.dumps(output, indent=2, sort_keys=True)) + + if __name__ == "__main__": main() diff --git a/tools/scripts/bench_subsystems.py b/tools/scripts/bench_subsystems.py index 0909d0258..45721f7fe 100644 --- a/tools/scripts/bench_subsystems.py +++ b/tools/scripts/bench_subsystems.py @@ -8,9 +8,11 @@ uv run python tools/scripts/bench_subsystems.py """ +import gc import io import json import sqlite3 +import statistics import tempfile import time import timeit @@ -26,10 +28,436 @@ __all__ = ("SubsystemBenchmark", "main", "print_results_table", "run_benchmarks") +STUB_COMPLEX_QUERY_SQL = """ +WITH recent_events AS ( + SELECT user_id, kind, COUNT(*) AS cnt, SUM(amount) AS total_amount + FROM events + WHERE ts >= ? + GROUP BY user_id, kind +) +SELECT u.id, u.name, o.tier, o.region, r.kind, r.cnt, r.total_amount +FROM users u +JOIN orgs o ON o.id = u.org_id +LEFT JOIN recent_events r ON r.user_id = u.id +WHERE u.status = 'active' AND o.region = 'us' +ORDER BY r.total_amount DESC +LIMIT 25 +""" + + +class SubsystemBenchmark: + """Container for a single subsystem benchmark.""" + + __slots__ = ("bench_fn", "description", "iterations", "name", "setup_fn") + + def __init__( + self, name: str, bench_fn: Any, iterations: int = 10_000, setup_fn: Any = None, description: str = "" + ) -> None: + self.name = name + self.bench_fn = bench_fn + self.iterations = iterations + self.setup_fn = setup_fn + self.description = description + + +def run_benchmarks(db_path: Path, iterations: int, warmup: int, profile: bool = False) -> list[dict[str, Any]]: + """Run all subsystem benchmarks using interleaved round-robin rounds. + + Args: + db_path: Path to the temporary SQLite database. + iterations: Number of iterations per benchmark. + warmup: Number of warmup iterations. + profile: Whether to profile the benchmarks using HotPathProfiler. + + Returns: + List of result dictionaries with timing data. + """ + benchmarks = _build_benchmarks(db_path, iterations) + active_benchmarks: list[SubsystemBenchmark] = [] + cleanup_benchmarks: list[SubsystemBenchmark] = [] + for bench in benchmarks: + if bench.name == "_cleanup_": + cleanup_benchmarks.append(bench) + else: + active_benchmarks.append(bench) + + try: + for bench in active_benchmarks: + for _ in range(warmup): + bench.bench_fn() + + results: list[dict[str, Any]] = [] + if profile: + for bench in active_benchmarks: + click.echo(f" Benchmarking: {bench.name}...") + with HotPathProfiler() as prof: + elapsed = timeit.timeit(bench.bench_fn, number=bench.iterations) + click.echo(f" Profile results for {bench.name}:") + prof.print_report(limit=10) + click.echo() + time_per_op_us = (elapsed / bench.iterations) * 1_000_000 + ops_per_sec = bench.iterations / elapsed if elapsed > 0 else float("inf") + results.append({ + "name": bench.name, + "time_per_op_us": time_per_op_us, + "ops_per_sec": ops_per_sec, + "total_time": elapsed, + "iterations": bench.iterations, + "description": bench.description, + }) + return results + + rounds = min(5, max(1, iterations)) + per_round_us: dict[str, list[float]] = {bench.name: [] for bench in active_benchmarks} + total_elapsed: dict[str, float] = dict.fromkeys((bench.name for bench in active_benchmarks), 0.0) + total_iters: dict[str, int] = dict.fromkeys((bench.name for bench in active_benchmarks), 0) + + for _ in range(rounds): + for bench in active_benchmarks: + batch_size = max(1, bench.iterations // rounds) + gc.collect() + gc_was_enabled = gc.isenabled() + if gc_was_enabled: + gc.disable() + try: + elapsed = timeit.timeit(bench.bench_fn, number=batch_size) + finally: + if gc_was_enabled: + gc.enable() + per_round_us[bench.name].append((elapsed / batch_size) * 1_000_000) + total_elapsed[bench.name] += elapsed + total_iters[bench.name] += batch_size + + for bench in active_benchmarks: + click.echo(f" Benchmarking: {bench.name}...") + samples = per_round_us[bench.name] + median_us = statistics.median(samples) if samples else 0.0 + ops_per_sec = 1_000_000.0 / median_us if median_us > 0 else float("inf") + results.append({ + "name": bench.name, + "time_per_op_us": median_us, + "ops_per_sec": ops_per_sec, + "total_time": total_elapsed[bench.name], + "iterations": total_iters[bench.name], + "description": bench.description, + }) + + return results + finally: + for bench in cleanup_benchmarks: + if bench.setup_fn: + bench.setup_fn() + -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- +def print_results_table(results: list[dict[str, Any]]) -> None: + """Print benchmark results as a rich table. + + Args: + results: List of result dictionaries from run_benchmarks. + """ + console = Console() + + subsystem_times = [ + r["time_per_op_us"] + for r in results + if not r["name"].startswith("raw ") and r["name"] != "session.execute() - full path" + ] + total_subsystem_us = sum(subsystem_times) if subsystem_times else 1.0 + + table = Table(title="Subsystem Micro-Benchmarks (sqlite)") + table.add_column("Subsystem", style="cyan", no_wrap=True, max_width=52) + table.add_column("Time/Op", justify="right", style="yellow") + table.add_column("Ops/sec", justify="right", style="green") + table.add_column("Relative Cost", justify="right", style="magenta") + + for r in results: + name = r["name"] + us = r["time_per_op_us"] + ops = r["ops_per_sec"] + pct = (us / total_subsystem_us) * 100 if total_subsystem_us > 0 else 0 + + _us_per_ms = 1000 + if us >= _us_per_ms: + time_str = f"{us / _us_per_ms:.2f} ms" + elif us >= 1: + time_str = f"{us:.2f} us" + else: + time_str = f"{us * 1000:.1f} ns" + + _million = 1_000_000 + _thousand = 1_000 + if ops >= _million: + ops_str = f"{ops / _million:.2f}M" + elif ops >= _thousand: + ops_str = f"{ops / _thousand:.1f}K" + else: + ops_str = f"{ops:.0f}" + + if name.startswith("raw "): + pct_str = "(baseline)" + elif name == "session.execute() - full path": + pct_str = "(end-to-end)" + else: + pct_str = f"{pct:.1f}%" + + table.add_row(name, time_str, ops_str, pct_str) + + console.print(table) + + console.print() + full_execute = next((r for r in results if r["name"] == "session.execute() - full path"), None) + raw_execute = next((r for r in results if r["name"] == "raw sqlite3 conn.execute()"), None) + + if full_execute and raw_execute: + overhead_us = full_execute["time_per_op_us"] - raw_execute["time_per_op_us"] + overhead_pct = (overhead_us / raw_execute["time_per_op_us"]) * 100 if raw_execute["time_per_op_us"] > 0 else 0 + console.print(f" [bold]Per-call overhead:[/bold] {overhead_us:.2f} us ({overhead_pct:.1f}% vs raw)") + console.print(f" [bold]Raw execute:[/bold] {raw_execute['time_per_op_us']:.2f} us/op") + console.print(f" [bold]sqlspec execute:[/bold] {full_execute['time_per_op_us']:.2f} us/op") + + +@click.command() +@click.option("--iterations", default=10_000, show_default=True, help="Number of iterations per subsystem benchmark") +@click.option("--warmup", default=100, show_default=True, help="Number of warmup iterations (not timed)") +@click.option("--profile", is_flag=True, default=False, help="Profile the benchmarks using HotPathProfiler") +@click.option("--json-output", default=None, type=click.Path(), help="Write subsystem results to a JSON file") +def main(iterations: int, warmup: int, profile: bool, json_output: str | None) -> None: + """Run subsystem micro-benchmarks for sqlspec hot path profiling. + + Isolates each execution subsystem and measures it independently to + identify exactly where time is spent during query execution. + """ + console = Console() + console.print("[bold]sqlspec Subsystem Micro-Benchmarks[/bold]") + console.print(f" iterations={iterations}, warmup={warmup}, profile={profile}") + console.print() + + db_path = _make_temp_db() + try: + _setup_test_table(db_path) + click.echo("Running benchmarks...") + results = run_benchmarks(db_path, iterations, warmup, profile=profile) + click.echo() + print_results_table(results) + if json_output is not None: + _write_json_results(results, json_output, iterations=iterations, warmup=warmup, profile=profile) + click.secho(f"Subsystem report written to {json_output}", fg="green") + finally: + with suppress(OSError): + db_path.unlink() + + click.echo() + click.secho("Subsystem benchmarks complete.", fg="green") + + +class _StubArrowTable: + """Minimal no-op Arrow table stub for pre-Phase-5 DuckDB dispatch_execute.""" + + __slots__ = ("column_names", "num_rows") + + def __init__(self) -> None: + self.column_names = ["id", "value"] + self.num_rows = 1 + + def to_pylist(self) -> list[dict[str, Any]]: + return [{"id": 1, "value": "value_1"}] + + +class _StubDuckDBCursor: + """Minimal no-op DB-API cursor isolating pure-Python SQLSpec driver overhead.""" + + __slots__ = ("_arrow_table", "description", "rowcount") + + def __init__(self) -> None: + self.description = [ + ("id", "NUMBER", None, None, None, None, None), + ("value", "STRING", None, None, None, None, None), + ] + self.rowcount = 1 + self._arrow_table = _StubArrowTable() + + def execute(self, sql: str, parameters: Any = None) -> "_StubDuckDBCursor": + return self + + def executemany(self, sql: str, parameters: Any = None) -> "_StubDuckDBCursor": + return self + + def fetchall(self) -> list[tuple[int, str]]: + return [(1, "value_1")] + + def to_arrow_table(self) -> _StubArrowTable: + return self._arrow_table + + def close(self) -> None: + return None + + +class _StubDuckDBConnection: + """Minimal no-op DB-API connection returning a reusable stub cursor.""" + + __slots__ = ("_cursor", "description", "rowcount") + + def __init__(self) -> None: + self._cursor = _StubDuckDBCursor() + self.description = self._cursor.description + self.rowcount = 1 + + def cursor(self) -> _StubDuckDBCursor: + return self._cursor + + def execute(self, sql: str, parameters: Any = None) -> _StubDuckDBCursor: + return self._cursor + + def executemany(self, sql: str, parameters: Any = None) -> _StubDuckDBCursor: + return self._cursor + + def fetchall(self) -> list[tuple[int, str]]: + return self._cursor.fetchall() + + def to_arrow_table(self) -> _StubArrowTable: + return self._cursor.to_arrow_table() + + def register(self, name: str, table: Any) -> None: + return None + + def unregister(self, name: str) -> None: + return None + + def commit(self) -> None: + return None + + def rollback(self) -> None: + return None + + +def _build_stub_driver_benchmarks(iterations: int) -> list[SubsystemBenchmark]: + """Build pure-Python stub-connection driver micro-benchmarks.""" + from sqlspec.adapters.duckdb import DuckDBConfig + from sqlspec.adapters.duckdb.driver import DuckDBDriver + + stub_conn = _StubDuckDBConnection() + shared_config = DuckDBConfig( + connection_config={"database": ":memory:"}, driver_features={"enable_arrow_ingest": False} + ) + shared_stmt_cfg = shared_config.statement_config + shared_features = dict(shared_config.driver_features) + stub_driver = DuckDBDriver( + connection=stub_conn, # type: ignore[arg-type] + statement_config=shared_stmt_cfg, + driver_features=shared_features, + ) + + select_sql = "SELECT id, value FROM test WHERE value = ?" + insert_sql = "INSERT INTO test (value) VALUES (?)" + no_param_sql = "SELECT 1" + scalar_sql = "SELECT id, value FROM test WHERE id = ?" + many_sql = "INSERT INTO test (id, value) VALUES (?, ?)" + batch_100: list[tuple[int, str]] = [(idx, f"value_{idx}") for idx in range(100)] + + stub_driver.execute(select_sql, ("value_1",)) + stub_driver.execute(select_sql, ("value_1",)) + stub_driver.execute(insert_sql, ("value_1",)) + stub_driver.execute(insert_sql, ("value_1",)) + stub_driver.fetch_one_or_none(select_sql, ("value_1",)) + stub_driver.execute(no_param_sql) + stub_driver.execute(scalar_sql, 1) + stub_driver.execute(STUB_COMPLEX_QUERY_SQL, 1) + stub_driver.execute_many(many_sql, batch_100) + + def bench_stub_fast_select() -> None: + stub_driver.execute(select_sql, ("value_1",)) + + def bench_stub_fast_insert() -> None: + stub_driver.execute(insert_sql, ("value_1",)) + + def bench_stub_fast_fetch_one_or_none() -> None: + stub_driver.fetch_one_or_none(select_sql, ("value_1",)) + + def bench_stub_no_param_select() -> None: + stub_driver.execute(no_param_sql) + + def bench_stub_scalar_select() -> None: + stub_driver.execute(scalar_sql, 1) + + def bench_stub_complex_cte() -> None: + stub_driver.execute(STUB_COMPLEX_QUERY_SQL, 1) + + def bench_stub_session_per_request_simple() -> None: + drv = DuckDBDriver( + connection=stub_conn, # type: ignore[arg-type] + statement_config=shared_stmt_cfg, + driver_features=shared_features, + ) + drv.execute(select_sql, ("value_1",)) + + def bench_stub_session_per_request_complex() -> None: + drv = DuckDBDriver( + connection=stub_conn, # type: ignore[arg-type] + statement_config=shared_stmt_cfg, + driver_features=shared_features, + ) + drv.execute(STUB_COMPLEX_QUERY_SQL, 1) + + def bench_stub_execute_many_100() -> None: + stub_driver.execute_many(many_sql, batch_100) + + bounded_iters = max(1, min(iterations, 2_000)) + return [ + SubsystemBenchmark( + name="stub driver: fast-path execute(SELECT, tuple)", + bench_fn=bench_stub_fast_select, + iterations=iterations, + description="Pure-Python driver overhead on warm fast-path SELECT with 1-tuple param", + ), + SubsystemBenchmark( + name="stub driver: fast-path execute(INSERT, tuple)", + bench_fn=bench_stub_fast_insert, + iterations=iterations, + description="Pure-Python driver overhead on warm fast-path INSERT with 1-tuple param", + ), + SubsystemBenchmark( + name="stub driver: fast-path fetch_one_or_none(SELECT, tuple)", + bench_fn=bench_stub_fast_fetch_one_or_none, + iterations=iterations, + description="Pure-Python driver overhead on warm fetch_one_or_none with 1-tuple param", + ), + SubsystemBenchmark( + name="stub driver: parameterless execute(SELECT)", + bench_fn=bench_stub_no_param_select, + iterations=iterations, + description="Pure-Python driver overhead on parameterless SELECT 1", + ), + SubsystemBenchmark( + name="stub driver: scalar-param execute(SELECT, int)", + bench_fn=bench_stub_scalar_select, + iterations=iterations, + description="Pure-Python driver overhead on SELECT with scalar positional param", + ), + SubsystemBenchmark( + name="stub driver: complex CTE+2-join execute(SELECT, int)", + bench_fn=bench_stub_complex_cte, + iterations=bounded_iters, + description="Pure-Python driver overhead on CTE + 2-join query with scalar param", + ), + SubsystemBenchmark( + name="stub driver: session-per-request simple SELECT", + bench_fn=bench_stub_session_per_request_simple, + iterations=bounded_iters, + description="New driver instance per request executing simple SELECT", + ), + SubsystemBenchmark( + name="stub driver: session-per-request complex CTE SELECT", + bench_fn=bench_stub_session_per_request_complex, + iterations=bounded_iters, + description="New driver instance per request executing CTE + 2-join SELECT", + ), + SubsystemBenchmark( + name="stub driver: execute_many 100 rows", + bench_fn=bench_stub_execute_many_100, + iterations=bounded_iters, + description="Pure-Python driver overhead on execute_many with 100 parameter rows", + ), + ] def _make_temp_db() -> Path: @@ -61,26 +489,6 @@ def _librt_string_writer_type() -> "type[Any] | None": return StringWriter -# --------------------------------------------------------------------------- -# Benchmark definitions -# --------------------------------------------------------------------------- - - -class SubsystemBenchmark: - """Container for a single subsystem benchmark.""" - - __slots__ = ("bench_fn", "description", "iterations", "name", "setup_fn") - - def __init__( - self, name: str, bench_fn: Any, iterations: int = 10_000, setup_fn: Any = None, description: str = "" - ) -> None: - self.name = name - self.bench_fn = bench_fn - self.iterations = iterations - self.setup_fn = setup_fn - self.description = description - - def _build_librt_candidate_benchmarks(iterations: int) -> list[SubsystemBenchmark]: """Build optional benchmarks for candidate librt.strings replacements.""" string_writer_type = _librt_string_writer_type() @@ -161,7 +569,7 @@ def bench_parameter_converter_string_writer() -> None: def bench_splitter_join_baseline() -> None: statement_chars: list[str] = [] for token_value in token_values: - statement_chars.append(token_value) # noqa: PERF402 - Keep the splitter's token append shape. + statement_chars.append(token_value) # noqa: PERF402 "".join(statement_chars).strip() benchmarks.append( @@ -248,10 +656,14 @@ def _build_benchmarks(db_path: Path, iterations: int) -> list[SubsystemBenchmark """ benchmarks: list[SubsystemBenchmark] = [] - # --- 1. SQL object construction --- - - from sqlspec import sql + from sqlspec import SQLSpec, sql + from sqlspec.adapters.sqlite import SqliteConfig + from sqlspec.adapters.sqlite.driver import SqliteCursor + from sqlspec.core.result._base import SQLResult from sqlspec.core.statement import SQL + from sqlspec.storage.pipeline import SyncStoragePipeline, _decode_arrow_payload, _encode_row_payload + from sqlspec.storage.registry import StorageRegistry + from sqlspec.typing import Empty def bench_sql_construction_no_params() -> None: SQL("INSERT INTO test (value) VALUES (?)") @@ -305,15 +717,9 @@ def bench_builder_same_shape_varying_values() -> None: ) ) - # --- 2. SQL.compile() --- - insert_stmt = SQL("INSERT INTO test (value) VALUES (?)", ("hello",)) def bench_compile_insert() -> None: - insert_stmt._processed_state = type(insert_stmt)._processed_state - # Reset to force recompilation - from sqlspec.typing import Empty - insert_stmt._processed_state = Empty insert_stmt.compile() @@ -329,8 +735,6 @@ def bench_compile_insert() -> None: select_stmt = SQL("SELECT * FROM test WHERE value = ?", ("value_1",)) def bench_compile_select() -> None: - from sqlspec.typing import Empty - select_stmt._processed_state = Empty select_stmt.compile() @@ -352,8 +756,6 @@ def bench_compile_select() -> None: complex_stmt = SQL(complex_sql, ("active", "2024-01-01", 100, 50)) def bench_compile_complex() -> None: - from sqlspec.typing import Empty - complex_stmt._processed_state = Empty complex_stmt.compile() @@ -366,30 +768,19 @@ def bench_compile_complex() -> None: ) ) - # --- 3. Query cache cycle --- - # We need a real SqliteDriver to test the QC path. - - from sqlspec import SQLSpec - from sqlspec.adapters.sqlite import SqliteConfig - spec = SQLSpec() config = SqliteConfig(connection_config={"database": str(db_path), "isolation_level": None}) - # We need to get a driver instance to test QC methods. - # Use the session context to get the driver, but keep it alive. _session_ctx = spec.provide_session(config) session = _session_ctx.__enter__() - # Create the test table and seed data through the sqlspec session session.execute("CREATE TABLE IF NOT EXISTS test (value TEXT)") data = [(f"value_{i}",) for i in range(100)] session.execute_many("INSERT INTO test (value) VALUES (?)", data) - # Prime the query cache by executing a query twice (first stores, second hits cache) session.execute("INSERT INTO test (value) VALUES (?)", ("cache_prime",)) session.execute("INSERT INTO test (value) VALUES (?)", ("cache_prime2",)) - # Now benchmark the cache-miss lookup path def bench_cache_execution_miss() -> None: session._cached_execution("INSERT INTO unique_table (col) VALUES (?)", ("val",)) @@ -402,7 +793,6 @@ def bench_cache_execution_miss() -> None: ) ) - # Full statement cache lookup cycle def bench_cached_execution() -> None: session._cached_execution("INSERT INTO test (value) VALUES (?)", ("bench_val",)) @@ -415,8 +805,6 @@ def bench_cached_execution() -> None: ) ) - # --- 4. Parameter processing --- - def bench_prepare_driver_params_tuple() -> None: session.prepare_driver_parameters(("value_1",), session.statement_config, is_many=False) @@ -453,10 +841,6 @@ def bench_driver_parameters() -> None: ) ) - # --- 5. Result construction --- - - from sqlspec.core.result._base import SQLResult - result_stmt = SQL("INSERT INTO test (value) VALUES (?)", ("hello",)) result_stmt.compile() @@ -494,11 +878,6 @@ def bench_result_init_select() -> None: ) ) - # --- 6. Storage runtime + Arrow boundary helpers --- - - from sqlspec.storage.pipeline import SyncStoragePipeline, _decode_arrow_payload, _encode_row_payload - from sqlspec.storage.registry import StorageRegistry - storage_root = db_path.parent / "bench-storage-runtime" storage_root.mkdir(exist_ok=True) storage_path = storage_root / "bench-storage.jsonl" @@ -548,13 +927,9 @@ def bench_storage_decode_jsonl_arrow() -> None: ) ) - # --- 7. Cursor context manager overhead --- - raw_conn = sqlite3.connect(str(db_path), isolation_level=None) raw_conn.execute("PRAGMA journal_mode = WAL") - from sqlspec.adapters.sqlite.driver import SqliteCursor - def bench_cursor_context_manager() -> None: with SqliteCursor(raw_conn) as _cursor: pass @@ -568,7 +943,6 @@ def bench_cursor_context_manager() -> None: ) ) - # Raw cursor for comparison def bench_raw_cursor() -> None: cursor = raw_conn.cursor() cursor.close() @@ -582,8 +956,6 @@ def bench_raw_cursor() -> None: ) ) - # --- 8. Full execute() overhead (single statement, end-to-end) --- - def bench_full_execute() -> None: session.execute("INSERT INTO test (value) VALUES (?)", ("bench_e2e",)) @@ -596,7 +968,6 @@ def bench_full_execute() -> None: ) ) - # Raw sqlite for comparison def bench_raw_execute() -> None: raw_conn.execute("INSERT INTO test (value) VALUES (?)", ("bench_raw",)) @@ -609,6 +980,7 @@ def bench_raw_execute() -> None: ) ) + benchmarks.extend(_build_stub_driver_benchmarks(iterations)) benchmarks.extend(_build_librt_candidate_benchmarks(iterations)) def cleanup_benchmarks() -> None: @@ -618,7 +990,6 @@ def cleanup_benchmarks() -> None: with suppress(OSError): storage_root.rmdir() - # Store session context for cleanup benchmarks.append( SubsystemBenchmark(name="_cleanup_", bench_fn=lambda: None, iterations=0, setup_fn=cleanup_benchmarks) ) @@ -626,134 +997,6 @@ def cleanup_benchmarks() -> None: return benchmarks -# --------------------------------------------------------------------------- -# Runner -# --------------------------------------------------------------------------- - - -def run_benchmarks(db_path: Path, iterations: int, warmup: int, profile: bool = False) -> list[dict[str, Any]]: - """Run all subsystem benchmarks and return results. - - Args: - db_path: Path to the temporary SQLite database. - iterations: Number of iterations per benchmark. - warmup: Number of warmup iterations. - profile: Whether to profile the benchmarks using HotPathProfiler. - - Returns: - List of result dictionaries with timing data. - """ - benchmarks = _build_benchmarks(db_path, iterations) - results: list[dict[str, Any]] = [] - - for bench in benchmarks: - if bench.name == "_cleanup_": - if bench.setup_fn: - bench.setup_fn() - continue - - click.echo(f" Benchmarking: {bench.name}...") - - # Warmup - for _ in range(warmup): - bench.bench_fn() - - # Timed run using timeit - if profile: - with HotPathProfiler() as prof: - elapsed = timeit.timeit(bench.bench_fn, number=bench.iterations) - click.echo(f" Profile results for {bench.name}:") - prof.print_report(limit=10) - click.echo() - else: - elapsed = timeit.timeit(bench.bench_fn, number=bench.iterations) - time_per_op_us = (elapsed / bench.iterations) * 1_000_000 # microseconds - ops_per_sec = bench.iterations / elapsed if elapsed > 0 else float("inf") - - results.append({ - "name": bench.name, - "time_per_op_us": time_per_op_us, - "ops_per_sec": ops_per_sec, - "total_time": elapsed, - "iterations": bench.iterations, - "description": bench.description, - }) - - return results - - -def print_results_table(results: list[dict[str, Any]]) -> None: - """Print benchmark results as a rich table. - - Args: - results: List of result dictionaries from run_benchmarks. - """ - console = Console() - - # Calculate total overhead as sum of key subsystem times (excluding raw baselines) - subsystem_times = [ - r["time_per_op_us"] - for r in results - if not r["name"].startswith("raw ") and r["name"] != "session.execute() - full path" - ] - total_subsystem_us = sum(subsystem_times) if subsystem_times else 1.0 - - table = Table(title="Subsystem Micro-Benchmarks (sqlite)") - table.add_column("Subsystem", style="cyan", no_wrap=True, max_width=42) - table.add_column("Time/Op", justify="right", style="yellow") - table.add_column("Ops/sec", justify="right", style="green") - table.add_column("Relative Cost", justify="right", style="magenta") - - for r in results: - name = r["name"] - us = r["time_per_op_us"] - ops = r["ops_per_sec"] - pct = (us / total_subsystem_us) * 100 if total_subsystem_us > 0 else 0 - - # Format time with appropriate unit - _us_per_ms = 1000 - if us >= _us_per_ms: - time_str = f"{us / _us_per_ms:.2f} ms" - elif us >= 1: - time_str = f"{us:.2f} us" - else: - time_str = f"{us * 1000:.1f} ns" - - # Format ops/sec - _million = 1_000_000 - _thousand = 1_000 - if ops >= _million: - ops_str = f"{ops / _million:.2f}M" - elif ops >= _thousand: - ops_str = f"{ops / _thousand:.1f}K" - else: - ops_str = f"{ops:.0f}" - - # Style for baselines - if name.startswith("raw "): - pct_str = "(baseline)" - elif name == "session.execute() - full path": - pct_str = "(end-to-end)" - else: - pct_str = f"{pct:.1f}%" - - table.add_row(name, time_str, ops_str, pct_str) - - console.print(table) - - # Print summary - console.print() - full_execute = next((r for r in results if r["name"] == "session.execute() - full path"), None) - raw_execute = next((r for r in results if r["name"] == "raw sqlite3 conn.execute()"), None) - - if full_execute and raw_execute: - overhead_us = full_execute["time_per_op_us"] - raw_execute["time_per_op_us"] - overhead_pct = (overhead_us / raw_execute["time_per_op_us"]) * 100 if raw_execute["time_per_op_us"] > 0 else 0 - console.print(f" [bold]Per-call overhead:[/bold] {overhead_us:.2f} us ({overhead_pct:.1f}% vs raw)") - console.print(f" [bold]Raw execute:[/bold] {raw_execute['time_per_op_us']:.2f} us/op") - console.print(f" [bold]sqlspec execute:[/bold] {full_execute['time_per_op_us']:.2f} us/op") - - def _write_json_results( results: list[dict[str, Any]], output_path: str | Path, *, iterations: int, warmup: int, profile: bool ) -> None: @@ -771,44 +1014,5 @@ def _write_json_results( Path(output_path).write_text(json.dumps(output, indent=2, sort_keys=True)) -# --------------------------------------------------------------------------- -# CLI -# --------------------------------------------------------------------------- - - -@click.command() -@click.option("--iterations", default=10_000, show_default=True, help="Number of iterations per subsystem benchmark") -@click.option("--warmup", default=100, show_default=True, help="Number of warmup iterations (not timed)") -@click.option("--profile", is_flag=True, default=False, help="Profile the benchmarks using HotPathProfiler") -@click.option("--json-output", default=None, type=click.Path(), help="Write subsystem results to a JSON file") -def main(iterations: int, warmup: int, profile: bool, json_output: str | None) -> None: - """Run subsystem micro-benchmarks for sqlspec hot path profiling. - - Isolates each execution subsystem and measures it independently to - identify exactly where time is spent during query execution. - """ - console = Console() - console.print("[bold]sqlspec Subsystem Micro-Benchmarks[/bold]") - console.print(f" iterations={iterations}, warmup={warmup}, profile={profile}") - console.print() - - db_path = _make_temp_db() - try: - _setup_test_table(db_path) - click.echo("Running benchmarks...") - results = run_benchmarks(db_path, iterations, warmup, profile=profile) - click.echo() - print_results_table(results) - if json_output is not None: - _write_json_results(results, json_output, iterations=iterations, warmup=warmup, profile=profile) - click.secho(f"Subsystem report written to {json_output}", fg="green") - finally: - with suppress(OSError): - db_path.unlink() - - click.echo() - click.secho("Subsystem benchmarks complete.", fg="green") - - if __name__ == "__main__": main() diff --git a/uv.lock b/uv.lock index f3e53a276..b0aa33f16 100644 --- a/uv.lock +++ b/uv.lock @@ -425,66 +425,66 @@ wheels = [ [[package]] name = "ast-serialize" -version = "0.12.1" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c2/1c/7257e6ec9382843915ce475558ce4492ccb5ed39122c256bb369c27e2ebf/ast_serialize-0.12.1.tar.gz", hash = "sha256:5285a390caf1c44368ae270f037f797b91427d138b7d43cad0f1fda4c83518d9", size = 954408, upload-time = "2026-10-03T12:25:00.221Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/f7/e976169da322c009bb083a52d21e88fbfe5f071e1806e8c8361ab4ac477a/ast_serialize-0.12.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:e73255c9227fd74eac8a9b55c4049e8ad7b66d1f690bf827c98a86b2e594def7", size = 897232, upload-time = "2026-10-03T12:23:21.945Z" }, - { url = "https://files.pythonhosted.org/packages/e1/89/5545f6f4d38dd41b4e2a20050967ccd722508bc90fab0dfba463d8c8b994/ast_serialize-0.12.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:4655ef993e69e01bb47d2d99647de9bbb74af03938438656832cd010d95de348", size = 1235397, upload-time = "2026-10-03T12:23:23.835Z" }, - { url = "https://files.pythonhosted.org/packages/22/19/e9b839ef9b57626e15e20dd7cf764a9a6b50f9750f86d0a49bc3a971fb72/ast_serialize-0.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bac5a99a2c91dd823be9b8c44645694fccbb0750773cc5b27889a9f6f22fce89", size = 1216331, upload-time = "2026-10-03T12:23:25.557Z" }, - { url = "https://files.pythonhosted.org/packages/26/2a/d054d4ff8ba42472a22e3da6eb6dee0e69a32c477b5077eefdbada99f554/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6485e681625ed7a094221f16a7ff2ef154946112266a05cf83bde50c959ef345", size = 1281194, upload-time = "2026-10-03T12:23:27.349Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0c/c73eddfa180a7a4c1613c0f3d3ef020b05dca9b922ac08212463c33ad11f/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a513bc6f60980d01767f7cbe39b17ce0373e722824a74ce28d6cea49ee3c8460", size = 1287170, upload-time = "2026-10-03T12:23:29.333Z" }, - { url = "https://files.pythonhosted.org/packages/94/77/39dc75d8b718844859b64a9067c9df0cfce218ca45ea215fb24a1fda3cf7/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82866f3523d53ffca8d2a69a750bec52908b69f728012e40959bebce2620453c", size = 1531238, upload-time = "2026-10-03T12:23:30.954Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c0/6a6a6f94f45a288c4bac2eb8379a3d9654574a0f9249380ce3b07f6d64bb/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:243054a05a5190f5d087b5c8b16423e7f1cefa4991bac26e9c8ace18074b75b6", size = 1307154, upload-time = "2026-10-03T12:23:32.645Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3d/80f843892bd0f7c0d95ec5422ba3dc315c1ce011e6f08b06d5f71bd82c25/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9fbe5a3e8acddfc2fddff3dbbc7ea0e9798b3df3428f851b8abc52a3806f31", size = 1301506, upload-time = "2026-10-03T12:23:34.63Z" }, - { url = "https://files.pythonhosted.org/packages/df/cc/49a5fe852706f545e3e005584c5be89456bc637a8c9179aeaa8b9f26e8e4/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:d3d516da3463071d27e64caf54d88cba25cf4ad4afcc807e0bcf67743719f03e", size = 1297616, upload-time = "2026-10-03T12:23:36.377Z" }, - { url = "https://files.pythonhosted.org/packages/49/5c/1208c91d6e00cc43cc276bd6233c40c9b4ec3ef8537c83281dd5372cbdb8/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d6711adf11136c77e3a35517de9488a5081d1012874fae99c2876b64f4daace", size = 1354884, upload-time = "2026-10-03T12:23:38.035Z" }, - { url = "https://files.pythonhosted.org/packages/a9/80/2b5fc912ff0be64d8d61ff5dc7dc405c6311297a0e2039b848b7d14333f2/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cbe239bee4bd609186daf60b95b7b0f47146c7f7f55f6da83807d747d6fe753f", size = 1457911, upload-time = "2026-10-03T12:23:39.679Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f8/d720429bf8933efbd0cc2038c0a50b6267a585d503500845c44bc6c8ff66/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:5bbf582286c9dc6b4c544ef645dc99e4b3aa09db28892bc60344141f6926641f", size = 1563023, upload-time = "2026-10-03T12:23:41.585Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0a/99e6cc92bdbae5db60f84a14a0fb1ae77b6087e451d77808d87558162c9a/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:99e33c93efb5254a70c525b46038212371dfe5693d48eb2d0d5f17d936a263d7", size = 1556601, upload-time = "2026-10-03T12:23:43.361Z" }, - { url = "https://files.pythonhosted.org/packages/7a/05/59de9e16a2e333da534f30776d0f5e426034b64c67c17843425e3cc827d1/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:aa6c17a2b7f07e81fa8cfcc4aa7c832b3e57733853aebea113ab502f9b0963db", size = 1665039, upload-time = "2026-10-03T12:23:45.257Z" }, - { url = "https://files.pythonhosted.org/packages/33/83/35ed67a127167b484b42a071df440f84b14c0d20ea8f69dbed5cc96bfd98/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f896fa38e0af38821e1ab1425c5dee89e359623e165765bdeae7d0eb6909e76d", size = 1472263, upload-time = "2026-10-03T12:23:46.811Z" }, - { url = "https://files.pythonhosted.org/packages/c4/b0/3ab8613bbb690297f1bb687d780a248c486df0f4131b6a82044fcb49e438/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:af699e81fd7ce80b8b03945826d8ea23dd36d072f10d4613402da597ba4ee9c6", size = 1499242, upload-time = "2026-10-03T12:23:48.531Z" }, - { url = "https://files.pythonhosted.org/packages/9d/a0/a28894d3b06f8775cea8989f371bd9f72ce562c32bc807770e7fb920ce17/ast_serialize-0.12.1-cp314-cp314t-win32.whl", hash = "sha256:10b59afc108eb285146acb23d1b5ec0fc58bb3c09cb2ab8876402df06c373c3b", size = 1119488, upload-time = "2026-10-03T12:23:50.419Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b2/0c44952f4ba4e14bb7f60a5858e2960dfefb9884e6ff007aaf64337dba5c/ast_serialize-0.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:72e871f6995a066c1b19104f8a6b5832b1163adb9a8267c2aa4711fbb0f4d1f3", size = 1157434, upload-time = "2026-10-03T12:23:52.383Z" }, - { url = "https://files.pythonhosted.org/packages/1f/1e/cb594c63f46a01d53629af1c4f9e42cd02afcea1c2fe483e12f22743ebac/ast_serialize-0.12.1-cp314-cp314t-win_arm64.whl", hash = "sha256:3398e458047d21c9bc1b323fe5aab77c608dc9ddb65b2d44deebaff503a1f1eb", size = 1129094, upload-time = "2026-10-03T12:23:54.133Z" }, - { url = "https://files.pythonhosted.org/packages/16/05/ca16884f9498386f3646bb18be59f0e31d44e992d252d7d6f5e4f8ae1ee2/ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:410233de149ab8414cb27c6fc73e9d2baa35d6f971672d540d752060d980ffb4", size = 1235686, upload-time = "2026-10-03T12:23:55.863Z" }, - { url = "https://files.pythonhosted.org/packages/29/f2/34e87ed30e292cf365523712c4bcfef1967d9c3c2749de21b1f93b1fe0f3/ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:b9a2310845302f1a6bd45ae8a67d5760211103a8d66410b854bfa440d107e093", size = 1215598, upload-time = "2026-10-03T12:23:57.48Z" }, - { url = "https://files.pythonhosted.org/packages/f8/dc/c498f41c957b6ff31b97ed8ceccf3a84f85af7debca1125183cab95bb58b/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff65f40f49d5e1a1a043ba366081d59a4e26a9f5c1b07eb1170e172115da7ca", size = 1281911, upload-time = "2026-10-03T12:23:58.954Z" }, - { url = "https://files.pythonhosted.org/packages/dc/60/70ccefae9d88058c4c234bf0aed93f54aca36eb74087736e76e9515aee96/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:536d783c4d91331f094e0a892221e619be5ffbe6fb6640885f14d7f226ec90ca", size = 1287296, upload-time = "2026-10-03T12:24:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/08/e9/4fc697879c7128e29f9dab2ed19a9b586a56b621e5ea4aee2ae28c18e116/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c42d2d65f388d1960c5796231eb9bf5a988c46228633eb489605c4549ad16c52", size = 1532339, upload-time = "2026-10-03T12:24:02.053Z" }, - { url = "https://files.pythonhosted.org/packages/8a/9e/9e2bd489731602a94dbd0c576ebe1cc487a2d0f6127be743f44711166f0d/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5628a12acc875fe7a167910f18d101dd101c2a7b1e6c2b6f7289ffaff25805c", size = 1308988, upload-time = "2026-10-03T12:24:03.61Z" }, - { url = "https://files.pythonhosted.org/packages/3f/69/e9cae837bd766a66db6953ffb5fc7f04b1945e02b0a9e4c6a0b6acb08f17/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e855adfa5bb982b2e6fe09056b2d584f6dd4fce085d91a07d1155683751b6b5", size = 1303412, upload-time = "2026-10-03T12:24:05.62Z" }, - { url = "https://files.pythonhosted.org/packages/b2/1e/5ef8c62d5031d93187ed0d8dade5d942de9920c3fbd678c7652362b9a7a2/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:fafe1471e8aca6c87b4913b7b54ff97197adf702fbe28692284b929dfa62ff96", size = 1298466, upload-time = "2026-10-03T12:24:07.242Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f3/25ded60844a1a437edc840e597b6f81daf91dc4a26035416e14298d3a091/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a5cac246474d2a703147d1605a6ac5ba0fa9e0a443cf1cf42513adf4df02686f", size = 1355617, upload-time = "2026-10-03T12:24:09.125Z" }, - { url = "https://files.pythonhosted.org/packages/05/68/a0d3cc8d8042208a2cbef7b26483f4941b44dd5dd717bb19f20e4a4c0d66/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6e25cd319fb0d7b39fcac666784ec86708ccbc78d07a698b1400cf5ed40c045b", size = 1458949, upload-time = "2026-10-03T12:24:10.772Z" }, - { url = "https://files.pythonhosted.org/packages/df/a0/5e4d355c48a9f125b8bec7b1b98d4d2dcd8324ff1d4dfcb03678a03c1414/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:657a7354ea16ed4d29f8127ed477c6fee3915c111d135f020ca835a991438e90", size = 1562328, upload-time = "2026-10-03T12:24:12.404Z" }, - { url = "https://files.pythonhosted.org/packages/8a/5f/3d40f6a7908200f2f0ed9ce1bad130d00e06d0405baa7918d4a4299b25dd/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:9eb9de7e59621acdb3e66984f374272b33d56d15a04763e2fd0604211e1c8303", size = 1557751, upload-time = "2026-10-03T12:24:14.455Z" }, - { url = "https://files.pythonhosted.org/packages/3f/13/d53e5a7e299d6dbaeab23a424eea2c98c821b46ed7b05abbe14743beeb63/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:09cc4d3103c1fc97f6845ba307af1db9cde5226bef47f8843220dde83f2276ba", size = 1666238, upload-time = "2026-10-03T12:24:16.171Z" }, - { url = "https://files.pythonhosted.org/packages/37/5b/7638ee3ae35a64e4467160a37dc7565cddfe2a87f06cef2fd07c93cfd503/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:c9e2a592706fd791c2271ce9c8f4e38c98d3ea0b4a86b511e09a4fe3ac44ab37", size = 1472620, upload-time = "2026-10-03T12:24:18.053Z" }, - { url = "https://files.pythonhosted.org/packages/fc/e7/6e9e621e0e4a3be5a9a5f8b6961982964d2367013dbe64feb5adaa43e56d/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ac042e95a575432c1730ca4cb9183066a2074886a599f46c1f1b0955fb8198", size = 1500141, upload-time = "2026-10-03T12:24:20.036Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4f/3217da5b671711c09cc6be580095839cad539983662a6599405bd75c1a19/ast_serialize-0.12.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:b3cd105995942cc6163a229674a86161ba1305f646493efd59bd6723a357ee14", size = 1118382, upload-time = "2026-10-03T12:24:21.599Z" }, - { url = "https://files.pythonhosted.org/packages/80/1e/6074cf29dca8ceff27d50e845c88e7a2eaaa7f0b6f3972909e878d844737/ast_serialize-0.12.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:a9cd24a26126088693ca054547ea0a391398a29cf1a3a2bec1009b4b6acc8b82", size = 1158034, upload-time = "2026-10-03T12:24:23.316Z" }, - { url = "https://files.pythonhosted.org/packages/92/a0/81ce428f9f3f1ca45f8b62c9711c30452bf8190476e8685cea0f72d8d008/ast_serialize-0.12.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:9649cd903db0dc047906c6dd740784a2ba665d54f7e43ba31457edbce76c9493", size = 1129437, upload-time = "2026-10-03T12:24:25.044Z" }, - { url = "https://files.pythonhosted.org/packages/3a/d9/1c08adb90728607d0d07d188df4558ae863d688b4458087efe9fafeca458/ast_serialize-0.12.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:5ef62601db3ce5c23445132262a193075e211fb2fc87b46b7550dd351fac0976", size = 897419, upload-time = "2026-10-03T12:24:26.699Z" }, - { url = "https://files.pythonhosted.org/packages/80/fb/1eabd2c0673283054468b1c6cb539aeb877636d6c84b280279f2d7a177a9/ast_serialize-0.12.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:98d91cd3a6cb76a39512ee090a539d1e3206b732ad8150eb38918cffa1ddf515", size = 1241515, upload-time = "2026-10-03T12:24:28.493Z" }, - { url = "https://files.pythonhosted.org/packages/1b/d7/c56955934a431a0fa3e4e9aa7af4a53ceab2a61241005427545208945eb4/ast_serialize-0.12.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8a32f184ce3e4b1d0b06d642a1243281cf55b99e0323680b1b8f904029fd7700", size = 1228048, upload-time = "2026-10-03T12:24:30.556Z" }, - { url = "https://files.pythonhosted.org/packages/1e/4e/2b2ca4602baf92f842316ea617423402089df4fbd2ea42571ba28725ba46/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e62126ac2be2d9340ac1b3ee7a0466a883ecbed634cff0929c88ca0b671483b7", size = 1292087, upload-time = "2026-10-03T12:24:32.45Z" }, - { url = "https://files.pythonhosted.org/packages/d9/49/9ebd05218a87ca31f4f855d5e3df14239bba3c58f2aed9d02c7cba5d94f5/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f93a70fa9826c04ea9f2c3a880f87f4cca09a828144ed5084682abd28110980", size = 1295355, upload-time = "2026-10-03T12:24:34.423Z" }, - { url = "https://files.pythonhosted.org/packages/dc/09/6db7c4327e7a56aba805f7190d377a159fc0bf6bdefb410dc7860624dfa3/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1858887be56a64a2aea899423dfe43787c34c75c18b0d7497de8e618d54b2790", size = 1541231, upload-time = "2026-10-03T12:24:36.05Z" }, - { url = "https://files.pythonhosted.org/packages/ed/85/7ab6097e5fe23cd4657b0e5a2fabb4f789f91e441a3ee40b3ca8b79be238/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89a2bc39a820bc7785b60c53a5742b4e8dd4c1a599294e2dd68fae545883d44a", size = 1317324, upload-time = "2026-10-03T12:24:37.737Z" }, - { url = "https://files.pythonhosted.org/packages/c0/60/58961e7fd129e226ce36788fe328d20034f3105f5d3380df690050517737/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a9eaedf1db4857dad7cc47dd757ed70bfcc40b89d44d516c4a2f0d5033bd76", size = 1311809, upload-time = "2026-10-03T12:24:39.306Z" }, - { url = "https://files.pythonhosted.org/packages/15/c7/09d973db87d4575cba470fd80a3fa489322f7d6882546e43a4b21012468e/ast_serialize-0.12.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:c30b609e8fea426b310543126de876592236a25aa8ebd59f1e2b323dd52a4085", size = 1306678, upload-time = "2026-10-03T12:24:40.891Z" }, - { url = "https://files.pythonhosted.org/packages/84/27/84f69c22bcdaa5256b4fe43ff972fc117668fff8e68495807d5792eadcce/ast_serialize-0.12.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b1ad06513022cfa1337744959255af0ef16119d2beb1e547b67f37ad9433d4a", size = 1362009, upload-time = "2026-10-03T12:24:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/43/46/76ee342ef22cd6d82ccd6089d5e2f7163246de73d1816ccb4b6ec0550db6/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6add54b495e37ae3cf3a1f0d5eaba364814eb72e93026adc41b7791e4b0d45d3", size = 1467959, upload-time = "2026-10-03T12:24:44.496Z" }, - { url = "https://files.pythonhosted.org/packages/31/4d/18e48154bbf6058eed8d9b54fcebb2e130f8a380db1a2a202b4faac48626/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:5fc136cd08001b817ad0b3e7426f50a7d2b8982dc7c6491f0af78af4c3dd8672", size = 1571157, upload-time = "2026-10-03T12:24:46.156Z" }, - { url = "https://files.pythonhosted.org/packages/34/76/6b16ddf0510e713613a5f5441b13407c1bde6158df04946b9f1fdc65add3/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:45a9e6b700bbd973d49942668a5cdbeffa693f2e250b8a5409abd1fa9d351854", size = 1566873, upload-time = "2026-10-03T12:24:48.542Z" }, - { url = "https://files.pythonhosted.org/packages/75/33/9f6169ae7f60c2da4baec03450073d3f1edb39e95ab538be0d25a7d2f72e/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:a1d8267f83c613ea0a31f2518df074bd62e98a4b3a4892f6a529d74e08e02dba", size = 1673030, upload-time = "2026-10-03T12:24:50.566Z" }, - { url = "https://files.pythonhosted.org/packages/11/51/0d78755bd61d6cf8980f0cfdc7fa8ede38df46a5423c9f7a3da0cff587ec/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a0cbab9796e6ce841197feeba4008faa96b4cc7741129542fd81c882d7a4f01", size = 1480052, upload-time = "2026-10-03T12:24:52.277Z" }, - { url = "https://files.pythonhosted.org/packages/01/ae/ad4c0e5129991f2761f388420c5ded37cb134ec5882e3e59043d33c1ad87/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b4282695f1d51a3c6ef76560351bad5af880eff7d755aefea325bffb9bf68c25", size = 1508940, upload-time = "2026-10-03T12:24:53.846Z" }, - { url = "https://files.pythonhosted.org/packages/38/6b/3299182794d38815ae6e9c7ede9bb8f2e4aa93c3578bed201c1ea746643a/ast_serialize-0.12.1-cp39-abi3-win32.whl", hash = "sha256:119d1b0cadaba4a6e475f9bbe79eecc79351e373142eabe7c353f0250d70aebb", size = 1125618, upload-time = "2026-10-03T12:24:55.405Z" }, - { url = "https://files.pythonhosted.org/packages/86/14/5d4fb733c18a1d69e237c067b183842f3a7ea1c999a51ddc87093a281c88/ast_serialize-0.12.1-cp39-abi3-win_amd64.whl", hash = "sha256:3d6ed63d4fc1ec867b8cb522d58c36df0e8f05e487bea0ffd102043a37636d72", size = 1165798, upload-time = "2026-10-03T12:24:57.052Z" }, - { url = "https://files.pythonhosted.org/packages/f1/f4/b54123680025c0b7253117418f023d1b2487f1102552acbdd9d8ee96b622/ast_serialize-0.12.1-cp39-abi3-win_arm64.whl", hash = "sha256:610a41351de68199de9a1434499083b4256c0df7658ec1cfc0a0a7b20b08d317", size = 1136560, upload-time = "2026-10-03T12:24:58.689Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/d2/7c/dcde7ac261e0bfbb0360c207a1e3f9fd467f9bfaeaff7b62d01625bc86d2/ast_serialize-0.13.0.tar.gz", hash = "sha256:a0bdcef01e643e0810d2dedfb64d924bcfe079a15dc20d1c067870cc01d5c5e6", size = 954999, upload-time = "2026-10-12T17:06:22.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/86/6d1e047a41fc360ca4c7f8d1d3d0727302d7e0b0af4933a462c18390a00a/ast_serialize-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:4d1e15da4b6afc6fe80b87704be452aa0639df93d019a516e9ac9540357fc9b2", size = 897213, upload-time = "2026-10-12T17:04:48.544Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1e/4a55b8d219616dcb9523c062dd4110ca7dd9d3c99a0ea169ce1de049e8dc/ast_serialize-0.13.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:619050b18705310e19e254cdb7554289fe14da374cbfbb1362cd84635896fb7f", size = 1236396, upload-time = "2026-10-12T17:04:50.401Z" }, + { url = "https://files.pythonhosted.org/packages/be/ad/93ec0502d21691152a26103f76064c4a5bef6945866cadd8288485907306/ast_serialize-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7ce1b50c5a68233e890926405afc308a5f10f6f49ec3a094d3dfa8b6733e4496", size = 1217965, upload-time = "2026-10-12T17:04:52.692Z" }, + { url = "https://files.pythonhosted.org/packages/86/37/12412da699bdfde52233836c98a30abc6086dbe17bdf0ad3497788e719c3/ast_serialize-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bab08a6f287cd620578084f9974cfaf3bef71959105f62af85fa70298b24851", size = 1282094, upload-time = "2026-10-12T17:04:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/74/a4/42ba2ca24865442c6aea14bc648a0800181f76dde7e6e3f4036887cf7ce6/ast_serialize-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4e0bc018a457052d4638b469f90674e6ec0e32d86ac4a7bfa1f7d1c71a961426", size = 1287429, upload-time = "2026-10-12T17:04:55.546Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ec/3a5923554ea5f7148ea5742ffbef2f65deed68a8a5cbad6ca5f744adfcf2/ast_serialize-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:452fdaf5ff0b791870bb332254e083107d7abe29ef43251411c265c5b138f9a2", size = 1531892, upload-time = "2026-10-12T17:04:57.147Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b4/e61038bf0dfb42c208e96e3ef953855e3bdb435f015f9dbf14e721c98066/ast_serialize-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12441bc7e41e495db5634c98adc8f8886b619ce2f1e68effe3f792a2adca9f47", size = 1309544, upload-time = "2026-10-12T17:04:59.026Z" }, + { url = "https://files.pythonhosted.org/packages/46/a2/5fc3e341f00711931bae32af6937122e205173d8521d1f7282b6cc2cc0f1/ast_serialize-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84cd9efdd3cd780b1f5361049becd91e0bcb0f16c2c216f41ee82e728a98390b", size = 1301823, upload-time = "2026-10-12T17:05:00.671Z" }, + { url = "https://files.pythonhosted.org/packages/42/51/592a4640cb7a88cd27c457a0448c5a635cf5c01c78b693dd6ef4dfe4d3bf/ast_serialize-0.13.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4dc7a24c734aded0557ef90bfabd2278b37502aacde84f49b53b4cb6a0711ea9", size = 1298520, upload-time = "2026-10-12T17:05:02.229Z" }, + { url = "https://files.pythonhosted.org/packages/66/f6/7d938404c3b94e3a18b2b53a4250abf3f28f38caf46b6e381020e83977ab/ast_serialize-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c95c04f1781cbefe89d512ce30e051d10823827ae542d9f18d5c2e7ba0fad08", size = 1355363, upload-time = "2026-10-12T17:05:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2e/88d437318054d5ce5c6c2be477258ae8d756620463221276fb0ae267fe93/ast_serialize-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fadba24386498ed745c848b0d45e4939a506694bd2b474c57576e9640f3defe2", size = 1459864, upload-time = "2026-10-12T17:05:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6c/6e502e9d9e8296d621bcb9173ec4eda6498b80757c91974be3d6e89058d2/ast_serialize-0.13.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:771cf5ee8329ee8472dd7a3d8bea7dbc480032ed8ddb4d37d40b57b95ee19ef2", size = 1563855, upload-time = "2026-10-12T17:05:07.017Z" }, + { url = "https://files.pythonhosted.org/packages/70/0b/f162a027c5f0c7f3f782803ee58f10ba355e49ba3e7931649635bacf2c07/ast_serialize-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a1714ee591a8e19833c0530a89e5a9faa7f62a11fc88f62fc5b722c7425dcc1f", size = 1557563, upload-time = "2026-10-12T17:05:08.709Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/71b81259135a68122486ce1522ddb1d8acdd3c11df260a3e7a337d428d90/ast_serialize-0.13.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:220a993dfc8b173e7062f690f9e00f4ebefe56718171bf35dccd73a9f8cea100", size = 1664708, upload-time = "2026-10-12T17:05:10.505Z" }, + { url = "https://files.pythonhosted.org/packages/a7/dc/9851e6b6c4771fa0d4b10fce536d427f184f85a0fb83ae19633f4c9337b9/ast_serialize-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:446de6067d853f61b4bde8741762c83d06b57ea81f96dd47977714d9f31837fa", size = 1472153, upload-time = "2026-10-12T17:05:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/b5/5c/73bcf627607855408c500106baf849cd1cb24d2a95e480eda3c5245c908e/ast_serialize-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0cc1f94fcd3b67005a32ee3e4c6b27cdc41659f697840d00fbb1e815ec27044", size = 1500997, upload-time = "2026-10-12T17:05:14.15Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b4/ee6942656e725a1e5e15d8680425356a3c96c2fbfb519f476cd82261e93d/ast_serialize-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:77efef815ecae1195ac9889f616bd518d53b2173e87157043253b864af1c81c5", size = 1122428, upload-time = "2026-10-12T17:05:15.569Z" }, + { url = "https://files.pythonhosted.org/packages/23/9b/dacfec064d40c3be7051a95140bb4f343491aba0de06b612aec64a84a8bf/ast_serialize-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c77e5b62dfbfdfc1b025105f5038114ae988a0e616d48a845e0e57173f3f37c8", size = 1158940, upload-time = "2026-10-12T17:05:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/70/75/f65e883e7cd0e804fdda0ee690aeebbf3a28b96eebc1588f99e9a24a9b3e/ast_serialize-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:8e7c6fec7fe03cb8f40c4af81d742aa0cf690cf0b7bded47508a8a392093f414", size = 1133534, upload-time = "2026-10-12T17:05:19.13Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b4/a791bafbc7b9ecc3e727ab54651667372f3b33a6d1449c28783aeb1e3835/ast_serialize-0.13.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:9eaa20714acf43ef0a0c82850a2ec8097f834648f527c38f1483e2fd9a52cd3e", size = 1236335, upload-time = "2026-10-12T17:05:20.89Z" }, + { url = "https://files.pythonhosted.org/packages/30/c9/4a8d26f08d8053d4623fde6c83989b240120718cbea0b59727284fedd311/ast_serialize-0.13.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:3f5d4a7fcc916026010bae2a154e5be2c05040e67ef5c494c66a5cf315c41e30", size = 1217138, upload-time = "2026-10-12T17:05:22.586Z" }, + { url = "https://files.pythonhosted.org/packages/82/53/68ef7ef55880e2087dc585f319847bc6670192987aad2e3a79ba4fb700f9/ast_serialize-0.13.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:807875ed8c5de739c8c55a45944336fcb9b8601d77fcee384a743cd0497211d6", size = 1282493, upload-time = "2026-10-12T17:05:23.929Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e7/ef9e47cd8b11cc3cfed2b37d64d3166197db2aa03e9707d04af440b58c82/ast_serialize-0.13.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e611937e6e77448496489627ae2558b6f6143449b1fb33f8a495665212eee58a", size = 1287893, upload-time = "2026-10-12T17:05:25.5Z" }, + { url = "https://files.pythonhosted.org/packages/d6/58/3426be929c1888ef63e4defcb053e900ecee819c646db55c5a7f60de32ca/ast_serialize-0.13.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bb8dd779c0a25478fe1db1a8b06dd4dd5e66077d6d0afe354acadb6d9aee7d0", size = 1533846, upload-time = "2026-10-12T17:05:26.924Z" }, + { url = "https://files.pythonhosted.org/packages/0a/3d/1eab7d9974578c2d1c514934ed88d174bdd680780857958a72aad5b8c9d3/ast_serialize-0.13.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a49f2a01a6df3150e022087cec0bf0ad580fec8f38a17f124d07dbb115106d1", size = 1308580, upload-time = "2026-10-12T17:05:28.466Z" }, + { url = "https://files.pythonhosted.org/packages/50/a2/6164467e272527c107dd2209c6c4963c48a1b40c914a560c0ce77027566d/ast_serialize-0.13.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f30f0e59be30c0c9540e8908b14874bc8d3c1d52a4562a4cfb426b003bd6c28b", size = 1302242, upload-time = "2026-10-12T17:05:29.999Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/9024bdeead34b560f8278fb5ac96098be9a3d65ba74c6d7accd9cab21e0e/ast_serialize-0.13.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:be6b1a4ee49866c77eb8a50e9cbc845370e6c15230a126d0a71aeab35f31c78c", size = 1299515, upload-time = "2026-10-12T17:05:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d5/d1f5b9c0990fd0455d054c52bf461baa2b3ec4a93f6d8c633dbf895dd7fe/ast_serialize-0.13.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8da1a31e941adbea886a85fb25efc6f09353d58c665fdbc523a914e3d2e49fe", size = 1355301, upload-time = "2026-10-12T17:05:33.384Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0d/7c05f233ca26f1837a44880c2167de440191bc5d44c0f3c880162952d702/ast_serialize-0.13.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:3a9469e4b93d87e4ee8f5c7e9462f973032793a24b9ae37ffee860220f17586a", size = 1459499, upload-time = "2026-10-12T17:05:34.977Z" }, + { url = "https://files.pythonhosted.org/packages/06/72/04bf634442d7dc3246d77ac1738be7ee9524869e78097332c497178640a4/ast_serialize-0.13.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:192aed400b2b92ebe41da17e856b9b6e17dbcebe0011ce4c6370d4a8a0486233", size = 1564419, upload-time = "2026-10-12T17:05:37.641Z" }, + { url = "https://files.pythonhosted.org/packages/01/4d/0c693c6b60c2abd9e03450e37c88ec86e2ac57d7734f06c296f336a2f0cb/ast_serialize-0.13.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:ee58f0db40f121ff0820242b286702700bc0ec58a53b6ac43ce4f43714d42e0d", size = 1557221, upload-time = "2026-10-12T17:05:39.108Z" }, + { url = "https://files.pythonhosted.org/packages/44/35/f35b73694cf0daf539bfd74ab51502428b403d1ccc42358730b7e93ccd3f/ast_serialize-0.13.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:e241f68cf5060bff9b161b202b60d6d52161ff3777fe56eb6a9a6764fdb7fdd9", size = 1665471, upload-time = "2026-10-12T17:05:40.654Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/fd6ec20c4cc6063ca67671df8ce59ea9e1512deb149289e50f36b7d413a2/ast_serialize-0.13.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:bd89da715b857a27c33fad713ea0561912c56f773ebd0fed2760cedd99724dc6", size = 1473045, upload-time = "2026-10-12T17:05:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/da/53/8fc26dac873859e5b9fc3117cae2ed6b4afb6dc6f9e2d6fb62ee9fcade72/ast_serialize-0.13.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:6cbfa6dae34d5686056ef7c40ce1d3e7e1de48555e3a2fed985aef2d1f869d9a", size = 1501600, upload-time = "2026-10-12T17:05:44.089Z" }, + { url = "https://files.pythonhosted.org/packages/4f/35/363f4c3428b382d981d64fa871e468ddd9d48c9f0b969babbd462150ec98/ast_serialize-0.13.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:6a406251363eeb5c7b85a405eddd123e627a531bd150dc673a7f5dd087743b5c", size = 1120541, upload-time = "2026-10-12T17:05:45.595Z" }, + { url = "https://files.pythonhosted.org/packages/8b/07/df7ecee097d62214d042216fd2945207a10b681bb933daa64f238765cdb5/ast_serialize-0.13.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:cdd8fd066858b57ea2761b3d3989c90ea23913825bbb5453cc684c28bba3fb19", size = 1157906, upload-time = "2026-10-12T17:05:47.326Z" }, + { url = "https://files.pythonhosted.org/packages/be/4f/7781e45103d530f9fc963416e0a0beef6a2608a04d18290902ae6b074073/ast_serialize-0.13.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:841262622499585f0610a927434db526578553d8dae270b90d4c419a385e46b7", size = 1133990, upload-time = "2026-10-12T17:05:49.17Z" }, + { url = "https://files.pythonhosted.org/packages/6d/15/7284905fbd1600016f014ac3edc82199cc0f108f31d42221faa6067fe847/ast_serialize-0.13.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:efaab6400de8ee2d0e38695feccb0758acf11c1d0f8bc58a7090c566dd3ae88e", size = 897415, upload-time = "2026-10-12T17:05:50.722Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d1/2bbd7fe3ea2381410d0efb529e8af64027ea78bb58afad3c7ff641b33934/ast_serialize-0.13.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c51c855d8b7d5403925599acd3c6fc91b321eba3bf46dcc9fe88fd2d6619dac7", size = 1243056, upload-time = "2026-10-12T17:05:52.154Z" }, + { url = "https://files.pythonhosted.org/packages/09/b8/a7c8d5ccc0a31589750e476f629bd8681f9bd08421112c71a9c388bf8d7e/ast_serialize-0.13.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:d47c8f0eedf0a41681c10a7c497fef3691c4a6b4af4de6c93a4916bb29712554", size = 1227662, upload-time = "2026-10-12T17:05:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/64/54/4d64557c43abbd425b49c8e0c5b27093ee915c27e22bd46ea84c07d94908/ast_serialize-0.13.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d7376c611055f5ee44e5e13a2620dbc6846f47c583952c1704c8805154c2d2f", size = 1292779, upload-time = "2026-10-12T17:05:55.629Z" }, + { url = "https://files.pythonhosted.org/packages/7c/6f/93ab5d55402ede8444d01848cf35546c5e7ae24e9bedced6b17f530e2ef5/ast_serialize-0.13.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fe2a3c8480e8e5eb41eaa958279b8c530b77b45065423f4ebd5223e118293055", size = 1295902, upload-time = "2026-10-12T17:05:57.191Z" }, + { url = "https://files.pythonhosted.org/packages/f6/5f/7292ef873948a99d26c6648fe92f4911b10c0396216388adfdbfc79a70dc/ast_serialize-0.13.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98edcd24240fa217d903c8f221bc05575baf38a87a207264ee7c800d21eef5a4", size = 1542236, upload-time = "2026-10-12T17:05:59.002Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bf/4028224723d2038392ca176cfa1f982b15f12f4606d62a7f681550600c80/ast_serialize-0.13.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19b1e8f4c088ce91053df310b444fceeddb39f728ca7d04272c983646e36314b", size = 1319397, upload-time = "2026-10-12T17:06:00.527Z" }, + { url = "https://files.pythonhosted.org/packages/00/ef/8d710fb1a5cc0e1985d023bfdee62c77cccaec6be1dee2ed841074a1ee0b/ast_serialize-0.13.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f55668338bcb871e83ee21ba865fb08b7b4b13af9312742f9878c39f9d84ee3", size = 1312759, upload-time = "2026-10-12T17:06:02.096Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a3/d0cff408a37f253c1ce5fd061389217d4e2b19645fb3a4bd8f9582f5ed56/ast_serialize-0.13.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:a918572608ceb20fba8c2b83560f3d20be90effa4614589477b46d81672f0c3d", size = 1308696, upload-time = "2026-10-12T17:06:03.985Z" }, + { url = "https://files.pythonhosted.org/packages/41/c5/0a84554655da9c235e4f80a4799c6a3d7ee5d8c1ea16009077a5d0e6f289/ast_serialize-0.13.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b004c3bdab0beb45194cd66c0b8feea40d676e461d26296f9c791c8e3e1b7061", size = 1362306, upload-time = "2026-10-12T17:06:05.939Z" }, + { url = "https://files.pythonhosted.org/packages/b1/12/d85d300fb0a628b8183bd9301df624a819160b9fb7a3db3745c097ea2672/ast_serialize-0.13.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5e88733df5ffff9062b5ff2779cf402e0aa1a61ca3f7f84b577a1af2b7e09676", size = 1470459, upload-time = "2026-10-12T17:06:07.699Z" }, + { url = "https://files.pythonhosted.org/packages/c6/96/5201ebd0eaa758de11e9b4802e094d995c8d7306ce07bf62cc54ba396122/ast_serialize-0.13.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:017ddd4f22e727ef93e66df2d53340a6ff809b7e34cc2218f67918ae6239aad0", size = 1572881, upload-time = "2026-10-12T17:06:09.132Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/05e0447e112ebaabcfc5cbf86b6f2ea455e3e08b2aebbf1f957546fd6991/ast_serialize-0.13.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:4b2ee61692de03009e6a8f372f24fbc2d4b768a2f51ecd426bb84acdfd6da3d1", size = 1568287, upload-time = "2026-10-12T17:06:10.744Z" }, + { url = "https://files.pythonhosted.org/packages/69/04/eaa3aa0543a747375d2a6e0b58efbf20cf149d9c3014898c63b13aef70da/ast_serialize-0.13.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:dffcffa543c8fcfb1ca941038eeae23e7f97ad8994e4d6f81fbd658cfa8cb440", size = 1674375, upload-time = "2026-10-12T17:06:12.407Z" }, + { url = "https://files.pythonhosted.org/packages/6a/25/cf850c03635876e6c6e53cf853d18aaed1fecc8b4e9b8f8a9300c269dc90/ast_serialize-0.13.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3e20e9ca3952196b91798f77ef267c36c7b3470021f950aa361d9be003fb655f", size = 1482386, upload-time = "2026-10-12T17:06:13.944Z" }, + { url = "https://files.pythonhosted.org/packages/28/b3/305c5144eade6c39a871f6ab33d6cbb6fbf7367c89bd4860cdbfd3453c71/ast_serialize-0.13.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:359fcebc49f855bd189cf568235dc84eabf521e00d03fce43135cad6484906bc", size = 1512130, upload-time = "2026-10-12T17:06:15.849Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a8/0b26e4e4b16e3ddc9b77b6b42a29b0d6cf11a9342523ed7f0eaf4f1e1c45/ast_serialize-0.13.0-cp39-abi3-win32.whl", hash = "sha256:684e191dd41b08b0b92692a181380a70cd76e3b6469606a40aae1ca6dab39e7d", size = 1130669, upload-time = "2026-10-12T17:06:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/5d/09/862169d471439eb11961692f867b09f34f0560977cafa042305705d2d0a4/ast_serialize-0.13.0-cp39-abi3-win_amd64.whl", hash = "sha256:8f672c8e6d3b9ef6e365a5543aee2012247d1d58d948ceddb75b33a6679609ca", size = 1167803, upload-time = "2026-10-12T17:06:18.918Z" }, + { url = "https://files.pythonhosted.org/packages/d6/85/c1c583ec96be412c4119a0ba4354c1e3519464036dd8d2909536661bed6a/ast_serialize-0.13.0-cp39-abi3-win_arm64.whl", hash = "sha256:8aff1682f9fa3e119a1cf8ef47504d38f7019b22b79e0f2b5c2135b9532d14db", size = 1138203, upload-time = "2026-10-12T17:06:20.466Z" }, ] [[package]] @@ -1430,11 +1430,11 @@ wheels = [ [[package]] name = "extra-platforms" -version = "13.11.0" +version = "13.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/e9/805ffa0a5d7bbb2eee2297a2fe1c22e41b2900daa2f1144d335e827e2a1b/extra_platforms-13.11.0.tar.gz", hash = "sha256:79bd4a79df48a873c6cd46e2c7318a7604f3d07c7a14b405b22f63b1368eef11", size = 492121, upload-time = "2026-10-01T16:06:47.514Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/7e/b92eb5fe6c14b00c1469357622ee32e8b96ba8247b90b9da4cba3743ab32/extra_platforms-13.12.0.tar.gz", hash = "sha256:883feefe546402e1ccfe6f53b3f248dbaee3cd72d82e137549769db452c4712c", size = 494651, upload-time = "2026-10-12T18:13:30.702Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/51/faf97cdf3da209138b9b4f265bfb9bcc23394a9e5a405f380cca494586f3/extra_platforms-13.11.0-py3-none-any.whl", hash = "sha256:6ee4e9972dccc82951140c8acb166714a49e45b79c38157b4141203ff73abbc0", size = 107041, upload-time = "2026-10-01T16:06:45.824Z" }, + { url = "https://files.pythonhosted.org/packages/36/26/7f61812ba2cc540edd3889802e90deeee03cd9569fafe41c64f1e35e03f7/extra_platforms-13.12.0-py3-none-any.whl", hash = "sha256:68a0cc9a16be81e6a9795bf413da7e14b1e8707dee260a47bedff7b6a5db58b3", size = 107721, upload-time = "2026-10-12T18:13:28.764Z" }, ] [[package]] @@ -1738,15 +1738,15 @@ grpc = [ [[package]] name = "google-auth" -version = "2.61.0" +version = "2.62.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/0b/9788e913f2202da49068c27ce821eebcf96319240a89d7bb11f206d6471f/google_auth-2.61.0.tar.gz", hash = "sha256:37f0815967322e8c32b12bf422531e8b637cafdaae0acbb9141117cfe6a96f23", size = 398379, upload-time = "2026-10-07T20:13:14.827Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/b9/4b2528f30114b106e3c3a7298ba38d85e682fd1f83fe7c4a5b77c7677082/google_auth-2.62.0.tar.gz", hash = "sha256:0bef0ce54bdf9ce226c5d66e4264413bd918141c31bbe49fb52eac882f513d69", size = 398180, upload-time = "2026-10-12T19:20:48.328Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/68/c2ff6043d93fda69e5c12a834d0be3b3fa6c762e89732160532566be621b/google_auth-2.61.0-py3-none-any.whl", hash = "sha256:ca60266a37475ae68b63bac007272f46094b3d571abe92aded329b6cfb568025", size = 268021, upload-time = "2026-10-07T20:13:13.2Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a3/87fe9b7203ec2e56ea6a57c75e6d632eaf9487c7f7978cdad1d801bee0af/google_auth-2.62.0-py3-none-any.whl", hash = "sha256:4ff4319aeb4ad128409759d397a9fcafad126d0031d241cc0dd6b9a00b43e3f3", size = 267911, upload-time = "2026-10-12T19:20:46.355Z" }, ] [package.optional-dependencies] @@ -1933,7 +1933,7 @@ wheels = [ [[package]] name = "google-genai" -version = "2.29.0" +version = "2.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1947,9 +1947,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/2b/977616ef350e51d3d263eb43be23a91be793544cd937b51c0fac313be233/google_genai-2.29.0.tar.gz", hash = "sha256:124d3eef9ff6b9e4ea548f95ff5b7721683e7990c0a936b006d9e2ab61ea897d", size = 706902, upload-time = "2026-10-07T22:22:57.153Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/84/1d8817ac6999730ba0ce92cd2f0d373300ca8bafd0b3a432b8694737904e/google_genai-2.30.0.tar.gz", hash = "sha256:bd370da8366cef7efebdb4cee959b2180b402cb81ebd7a7fcfe0f00869dae73a", size = 736285, upload-time = "2026-10-13T00:38:19.253Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/27/a3dede4a1deb21f7fde6a171daa039bf0ea149a5e7826a785e8f109a6e13/google_genai-2.29.0-py3-none-any.whl", hash = "sha256:7adda55cb6746fb46d38ad3202c3372840b96eede964b5fb3df1469cd4998570", size = 1167603, upload-time = "2026-10-07T22:22:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b4/8cd54d2631371677f8157f45abce53f02fb19200b661451df618cd8ca851/google_genai-2.30.0-py3-none-any.whl", hash = "sha256:d59236ca3d5cbb93add212069d363d51eeea31b3628358f519b6a1d8f4bf43b4", size = 1198375, upload-time = "2026-10-13T00:38:16.299Z" }, ] [[package]] @@ -4482,7 +4482,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.14.0" +version = "2.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -4490,142 +4490,142 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6b/fb/6e44b63b26efea1cec48c26d8362313310202ef5ed6e7a52f1669e64e2cd/pydantic-2.14.0.tar.gz", hash = "sha256:8a51a7aaddd60f55566d1f07bdd87b92b463903f39a8f26b71a06314cd1548ae", size = 898613, upload-time = "2026-10-08T14:34:48.341Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/0b/8e10b2e693af8ec54346a14caf36221334775975a747c9ceaa3f8371d96d/pydantic-2.14.1.tar.gz", hash = "sha256:94f478203dd03404682a1ada216965651dd74b1d2d5ffd62e00e0837caab5c26", size = 900362, upload-time = "2026-10-11T18:37:55.396Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/eb/9146591cc819d040475bf7f2be786710c7f7eb8083693bf859728da2ca9c/pydantic-2.14.0-py3-none-any.whl", hash = "sha256:15fab1bea6f1dc5003b54fc2ecab230c1fd1dbade2acd4addc52d81e32416d4b", size = 487109, upload-time = "2026-10-08T14:34:46.864Z" }, + { url = "https://files.pythonhosted.org/packages/35/ea/a56b9fe5066f3537b7882f77e9c5dfb26d8c2eefdaed9b5fc73d57b4dc22/pydantic-2.14.1-py3-none-any.whl", hash = "sha256:9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454", size = 488106, upload-time = "2026-10-11T18:37:53.437Z" }, ] [[package]] name = "pydantic-core" -version = "2.50.0" +version = "2.50.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e6/6d/196e8c819e0e934f35a1a33b3530396feadb0af4ca38fe9f995249e55794/pydantic_core-2.50.0.tar.gz", hash = "sha256:84d2d38f7d163c4dec292f379e9de1960c661795442aca6c90d706436cb3749e", size = 513355, upload-time = "2026-10-08T14:30:58.245Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/99/114c2e71405da7610c884c64f8370f85b2fbb69bd427158a3f714515a803/pydantic_core-2.50.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:55684bc850059fc85ff8dc21a3e342b722d3178993c49ebdfdee8b698a432720", size = 2152583, upload-time = "2026-10-08T14:26:12.913Z" }, - { url = "https://files.pythonhosted.org/packages/75/a7/fabcfea81a5c3525507af4347fa3a50e51036d5db6611a63e47379776e56/pydantic_core-2.50.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3ae59461625518449f800acc4f874753ae96fcf993c47a27be07beb651371fd", size = 2045652, upload-time = "2026-10-08T14:26:14.38Z" }, - { url = "https://files.pythonhosted.org/packages/f5/80/226019008a041da423dc59d34f980b2b6e60a71971d07a8f52e10fc4747a/pydantic_core-2.50.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7080928078ff56c07da393392f772054e93a39c8033dc9c04547bd949f9b614", size = 2050325, upload-time = "2026-10-08T14:26:16.081Z" }, - { url = "https://files.pythonhosted.org/packages/b9/29/11c98cd9c49338f64aeb3d6e166c99b65d8949f00f285c5b66de75ade316/pydantic_core-2.50.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4b1bae96f41806b14dd8bb1568d39b5e33b5af7fb27f7be36c78b8fa84708917", size = 2189027, upload-time = "2026-10-08T14:26:17.581Z" }, - { url = "https://files.pythonhosted.org/packages/cf/b7/af7d9c4de715cc13e947d1135b73f0c181bce2f01e9f1d9e3c428f9ee31c/pydantic_core-2.50.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8facaf0a16121ac82cc403a71399a061b74624c1ae2034eec9ed6169c5d16ecb", size = 2388815, upload-time = "2026-10-08T14:26:19.306Z" }, - { url = "https://files.pythonhosted.org/packages/26/87/cd95dcd4d066180b2e22eff3dca01ac3af56607f9ecc43b7ba0748496f47/pydantic_core-2.50.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:610e9f483ac53c5cc59c2d3687224029b54eb494feec2d40a2bcfdd187635192", size = 2431821, upload-time = "2026-10-08T14:26:20.958Z" }, - { url = "https://files.pythonhosted.org/packages/f0/14/7122b78e9915e7dd50f9dcec6041a4e32874907b41b9e0880db2f1c7ae1a/pydantic_core-2.50.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac223ea031905319a64d0df116979ae438b42d6265862a3d63c9f9808c2905a7", size = 2157622, upload-time = "2026-10-08T14:26:22.947Z" }, - { url = "https://files.pythonhosted.org/packages/97/86/fcddead5bda54fc8efa0d80dbba5962e9a91193570e227404c7593707833/pydantic_core-2.50.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:75b451ec5e64a1d4b803317f34710131742ff2e42f4bf5403f597d0af857d7d8", size = 2254921, upload-time = "2026-10-08T14:26:24.607Z" }, - { url = "https://files.pythonhosted.org/packages/6c/e8/041d1c3e3e1f203a656ce592f8c1548e1ef2b0939d19c5ce66432c30e4bd/pydantic_core-2.50.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7eee44c3f1f8acc220a743a5ed5588949a4f18b33df4cfbef2b1277470285401", size = 2268163, upload-time = "2026-10-08T14:26:26.207Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ac/f515994ef1351e883276e85af0a50d839ead9d0d22743fa2c6dfd552f7ce/pydantic_core-2.50.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f44107b5fdfecc03438feb165431652c8006650471326e53dd86d4c19124f5c5", size = 2302005, upload-time = "2026-10-08T14:26:27.808Z" }, - { url = "https://files.pythonhosted.org/packages/04/6c/b970c4c87d92c4c974a5c5dc16bcf747f89b4e5729c5c45131267ef0421b/pydantic_core-2.50.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:718d05d4e078c7828f40ad3e310870fe4b837d93a32e7e8497a080c1f1040490", size = 2465387, upload-time = "2026-10-08T14:26:29.529Z" }, - { url = "https://files.pythonhosted.org/packages/4a/84/c0a92f97f9de7f133dfe694d0e8ca033a1c90fdf692dc69dfee3bf8a2e7b/pydantic_core-2.50.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:031867348c98ab49c6d3a16ad39738369ea48da59900d76d71c1e64de71fe79b", size = 2478673, upload-time = "2026-10-08T14:26:31.898Z" }, - { url = "https://files.pythonhosted.org/packages/f1/11/88e851761351cfef90382f175b07cede48edafc62a1509575dbc879934ef/pydantic_core-2.50.0-cp311-cp311-win32.whl", hash = "sha256:e41f9d1d9240e8e0d8a670ad3e66c0c00f0b1f7150a31bc6c445a4f87c1cb3ba", size = 2121424, upload-time = "2026-10-08T14:26:33.67Z" }, - { url = "https://files.pythonhosted.org/packages/c4/1f/89184f13b9ebaf6d8710cd0d1cfb466d7dc829de23468ab471b543dd07eb/pydantic_core-2.50.0-cp311-cp311-win_amd64.whl", hash = "sha256:90874428bc6678b26434336c77931c9734ecebedf50132c179205e5a9908761d", size = 2176278, upload-time = "2026-10-08T14:26:35.388Z" }, - { url = "https://files.pythonhosted.org/packages/ff/7a/5a630fff51218a860aa4e7699f781a7c9bf41c031987b613629c98f16618/pydantic_core-2.50.0-cp311-cp311-win_arm64.whl", hash = "sha256:b9be297ffe1015bfb2db4a23b6e1fec7e48361cf7c7b4f4f6bbdd008451b0a7b", size = 2183464, upload-time = "2026-10-08T14:26:37.485Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d8/e0fe374bc0082dfd337ca319505c9302ce383aeeeacc33d93383edd159e5/pydantic_core-2.50.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:79e8fc9c135ef628c45cb8aa5deef8d21de13d33bb4c59a859fa73d335ceb40a", size = 2222860, upload-time = "2026-10-08T14:26:39.575Z" }, - { url = "https://files.pythonhosted.org/packages/ef/7d/0a2f829e3e1d809393faab907e3d9307245cd6043ebf54fad439f72f0003/pydantic_core-2.50.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0abe1b44d361b948404b6b2ed80be2583e0077340572e071afa6e0eda4e1de30", size = 2025752, upload-time = "2026-10-08T14:26:41.786Z" }, - { url = "https://files.pythonhosted.org/packages/67/d4/e2808af12de4809dd6a4b532e3877baa5f1000df51bd8ebb8756450c6c92/pydantic_core-2.50.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae45853d25a23fba56681d2f9ed41f3e3f12f0a3b2393fefa08ff6406320a1f5", size = 2048432, upload-time = "2026-10-08T14:26:43.593Z" }, - { url = "https://files.pythonhosted.org/packages/18/58/46ad42a321051585d89f4e563b115015332caf79ba4e96f2785715c7f52d/pydantic_core-2.50.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd841dcf394ff261c26763a9d7be754176d4f1e6d26cb2a5d5331e91b6b56a5f", size = 2173565, upload-time = "2026-10-08T14:26:45.299Z" }, - { url = "https://files.pythonhosted.org/packages/92/69/541206e657ecea865e5f6d49a4a0d8062586d05da482d9d7ed77fdcdfc86/pydantic_core-2.50.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2e97985641fe53ad7824d1b5bfeb7990a5ff788c4559ee44d7522642560ddc2", size = 2361974, upload-time = "2026-10-08T14:26:47.024Z" }, - { url = "https://files.pythonhosted.org/packages/f6/c0/cf4850441d0d3d3736abe5e8b6fcb49a50166aa0651ed8444172f81f4b4f/pydantic_core-2.50.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7987866a2569396e6765c54d643fd6a7b3b234e89ee8d45a3729a7b4b2726145", size = 2440817, upload-time = "2026-10-08T14:26:48.937Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e2/2f793fa2f1b338aa03124efd4362956c84d7bf1b331dc86050bc0151e67d/pydantic_core-2.50.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f187030fc3d62c668feb0f09e92852e0eb414d7fcefc4748f2e67d245aade37e", size = 2160358, upload-time = "2026-10-08T14:26:50.74Z" }, - { url = "https://files.pythonhosted.org/packages/db/00/35e314c08e721ec1f070408a4e41926b90347ecd2863c08c78daad5f8a16/pydantic_core-2.50.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d187f43d1c5b844adc871c5b8c22b4aa12a116aaca4e9bd1521bc9ce479aae1f", size = 2231416, upload-time = "2026-10-08T14:26:52.733Z" }, - { url = "https://files.pythonhosted.org/packages/0f/3a/6f7c36afe35a9eca24e5f74f33ca641ac8cf308aae4ac7af4e585d464a40/pydantic_core-2.50.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cb4fcaabb28cabf21396a9b816b9afe091f8c776805fdf03e2cbc606b64dfa7c", size = 2242496, upload-time = "2026-10-08T14:26:54.485Z" }, - { url = "https://files.pythonhosted.org/packages/8d/af/77adf30285836c25f6a927e170df45f8dd4af713c89135b22abcbf3c6d67/pydantic_core-2.50.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1751e92d56fbb623b937d73985e500b1a2b1053e56f718a6c564660d99bb9cb0", size = 2279176, upload-time = "2026-10-08T14:26:56.432Z" }, - { url = "https://files.pythonhosted.org/packages/08/37/4e2a05247f82b59337bf45e9bb14aed85e2d83aecd21e49ad6cedf91882b/pydantic_core-2.50.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:70df7ff903aea05383298715ea53551c8f27c8f70cbe54ba7f05606cd822e7e4", size = 2450488, upload-time = "2026-10-08T14:26:58.218Z" }, - { url = "https://files.pythonhosted.org/packages/85/e6/75f25906212ecb94d69a0c8ed16f5c4ac312dc9961c2e93ff2a7fdf9a07a/pydantic_core-2.50.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:24302bf47319a64e5c5c7c29e971d2b7a20a190c64e59035a3df9582dec636fa", size = 2476664, upload-time = "2026-10-08T14:26:59.98Z" }, - { url = "https://files.pythonhosted.org/packages/75/72/ebb97b3becd0c0f2c722f698d34dec951ab1aa6086edf535f8769b545e3e/pydantic_core-2.50.0-cp312-cp312-win32.whl", hash = "sha256:5dbf9f18c8af11db719e67633be0af556d7d765bee0ca9419bd706fe4b7ed9fe", size = 2102409, upload-time = "2026-10-08T14:27:01.846Z" }, - { url = "https://files.pythonhosted.org/packages/3f/7b/5ebf3e62f5d0f6e3503ffdfffd7d0c2f8d45690afd37fa0cfdcb1786d7d6/pydantic_core-2.50.0-cp312-cp312-win_amd64.whl", hash = "sha256:1541c334af5d42cb9eb03862a9b4d2cfbfc670fd05172ec51f3ce02d704550f1", size = 2171536, upload-time = "2026-10-08T14:27:03.797Z" }, - { url = "https://files.pythonhosted.org/packages/58/1c/879ee9d5b63c60e5a077bab74ad93a4a48476090ca9c97534ab4263d0bae/pydantic_core-2.50.0-cp312-cp312-win_arm64.whl", hash = "sha256:b1399f918aea8fb76ffa99b474c9b768accea1f079fde407ee538bec89f20fa7", size = 2162946, upload-time = "2026-10-08T14:27:05.876Z" }, - { url = "https://files.pythonhosted.org/packages/81/25/f9a6958f73d92f66d620e3e1b091becf6b7a2ea89437118d397e2c6ca9ba/pydantic_core-2.50.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:049b0404792dcb942f1092bfdae5f819ef30445b0d174782e1909fbdd91bb48b", size = 2222904, upload-time = "2026-10-08T14:27:07.869Z" }, - { url = "https://files.pythonhosted.org/packages/c5/41/7f299b2ecf0ddbec8c2a68057ed53d29458ea5b0850615570849c454dd89/pydantic_core-2.50.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8f16bc5bb12f4c581b0f40facc28dbf286db32d1bf4e7e4f4c4e0f7f4e34ecf9", size = 2024863, upload-time = "2026-10-08T14:27:09.78Z" }, - { url = "https://files.pythonhosted.org/packages/89/db/a9852fa8780acca5dd81a21bb66d4b2fb41c39dc5672cec66c32cc7f13e9/pydantic_core-2.50.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1d084e92d0f4a096a5155b23d1ac603db8073ef98a3d1384badf1455e5ae742", size = 2047340, upload-time = "2026-10-08T14:27:11.85Z" }, - { url = "https://files.pythonhosted.org/packages/90/82/cd174e776e71ebcb1d289a57cb3e89565123a84c5708865e12efa82313f9/pydantic_core-2.50.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0af2d2f745ec00a6616c4dfba4477f9156ccfb45690f6ffa5fd34f42e871ed4", size = 2173461, upload-time = "2026-10-08T14:27:13.948Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5c/9c4b2aea09ec7b7af966e79631b1970f1adeb6a8ed001bc8bb64a2d1c23d/pydantic_core-2.50.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2f28a5a299d4cefd1066a6883d22600b9ae3606f881c3015d255784634647580", size = 2362629, upload-time = "2026-10-08T14:27:15.909Z" }, - { url = "https://files.pythonhosted.org/packages/28/d9/93afd007b61c50425c3c8f0402425779341c8d498bd224e5788eaf419c4c/pydantic_core-2.50.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bdd70c2d9e73bca09fad54605dfeaae2b4e770b2800c65f5f5342901ed567b9f", size = 2440134, upload-time = "2026-10-08T14:27:17.716Z" }, - { url = "https://files.pythonhosted.org/packages/c9/93/a4ef199535547aaa7c5d2cbf009a1d6a2b252885b14c6e70404e1c661716/pydantic_core-2.50.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e58acd43ac8d3905659c1d5309318dd576243e723dd7c3b5dd4f555479b77d4b", size = 2158480, upload-time = "2026-10-08T14:27:19.618Z" }, - { url = "https://files.pythonhosted.org/packages/de/20/f216e028d3bfbd9f8c67f7a2f0f8db04240926f630e2e66a5a8ac45fe00d/pydantic_core-2.50.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:5a07c644047f5abc268b4c39f8cb5a30e08a3c349228cb70142c7d3ed87587c0", size = 2231837, upload-time = "2026-10-08T14:27:21.742Z" }, - { url = "https://files.pythonhosted.org/packages/56/3f/57a6acf26e0acb82238ba4e25b35cbd6013b1d74eac99c4f358ff56bf6d6/pydantic_core-2.50.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8ad8dad549cd1645be591a50b4573995ee7e018f6619ddc2bc6ea44b2ad9f694", size = 2242240, upload-time = "2026-10-08T14:27:23.564Z" }, - { url = "https://files.pythonhosted.org/packages/94/41/f5f4014b40f91db4479e87dc038ac48a460a60baadea7ff9c942bc64dc62/pydantic_core-2.50.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:1773001198030e16f946a7ff7760fc3ebd45b12150f66fd0f433780043dc13d8", size = 2277852, upload-time = "2026-10-08T14:27:25.517Z" }, - { url = "https://files.pythonhosted.org/packages/f8/fa/485e4db093e10f29676fc696abf9d88f3626ee520e99cc488fb091222db9/pydantic_core-2.50.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:8ab9e74878172948e0426e3d27fba333fd6a1c3f9456d8e75643e8e6868ac1a1", size = 2450132, upload-time = "2026-10-08T14:27:27.493Z" }, - { url = "https://files.pythonhosted.org/packages/d3/48/fd07063cfc65e498528cea59b27b979eb5c890a54dcf5bb19ab0a589516d/pydantic_core-2.50.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e15bb1535f68a27f28e579ba3a8f74e7b05d350c45f5622b76a311d5a19ae48a", size = 2477011, upload-time = "2026-10-08T14:27:29.972Z" }, - { url = "https://files.pythonhosted.org/packages/fa/96/6f34c285dc87ed2fae5f1b53c51bb732eda21dc0c6c73d6a5d9dcc7033cd/pydantic_core-2.50.0-cp313-cp313-win32.whl", hash = "sha256:c21e6a6e4e6d32fb6acbc4f0fa69e8319cac0d65eeaa8298d757371cc2a9c687", size = 2102027, upload-time = "2026-10-08T14:27:32.497Z" }, - { url = "https://files.pythonhosted.org/packages/5e/50/dbdb3ba6699d494e59db2f145aeb97990e28d1503bc9f7bdd57eb4c15677/pydantic_core-2.50.0-cp313-cp313-win_amd64.whl", hash = "sha256:4f96ccd9368ecbf6685d8f6981584ab72b4f0ffd20685e747737e7e340277b8d", size = 2169974, upload-time = "2026-10-08T14:27:34.692Z" }, - { url = "https://files.pythonhosted.org/packages/7d/ae/cfe0e52a9b45b5ba12b3a839db928669543948760a059a72f13cf0346c29/pydantic_core-2.50.0-cp313-cp313-win_arm64.whl", hash = "sha256:a2b88f9f9fa52e1c34938ff1a18ee7fbffe482df0bb43c087a9a60578d273d68", size = 2162504, upload-time = "2026-10-08T14:27:36.989Z" }, - { url = "https://files.pythonhosted.org/packages/59/d9/6dd838672e5ccddf01556bfd1b4a6767e0c75abf9aaa4092cb87a56dbc4a/pydantic_core-2.50.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:62a93a9d206a3580c975c3c1f65a869cd063844d016c004f8d786a9309b3c591", size = 2224496, upload-time = "2026-10-08T14:27:39.281Z" }, - { url = "https://files.pythonhosted.org/packages/b3/20/c57d2efcc63fb8fb6ae9ec2818baf30e2a68751e8d1dedefaf3dab2e45fd/pydantic_core-2.50.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5a8403eef4a66743e339102fb3cdd8c8b9016b8bd67924685893d062c88896f9", size = 2023860, upload-time = "2026-10-08T14:27:41.332Z" }, - { url = "https://files.pythonhosted.org/packages/26/c8/f44ea3f1b00288f715e2320cbcd0f604104325370ef4bee42c5ed2ab076e/pydantic_core-2.50.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8354fbbe2abf0fb724b9303bef43bfd9b7a2779332813fa6d6559983954e7d8", size = 2049144, upload-time = "2026-10-08T14:27:43.695Z" }, - { url = "https://files.pythonhosted.org/packages/74/5c/2f5cf84ceaf6d7c351737124aebc135f50ce48a9dd28d2b5283103face54/pydantic_core-2.50.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00963fde61cf8880d9e7b5635a9830e0591edfa45168447fdc5b47635c0f6437", size = 2173456, upload-time = "2026-10-08T14:27:45.792Z" }, - { url = "https://files.pythonhosted.org/packages/97/de/dc0bd815a328e62b72bb93b5f3c762939670d4119fd27405b454970833da/pydantic_core-2.50.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bbd7da16d8b2912cc56c9d0b85c4998a6cbf39b220f81c0d8c397c64672ae0e", size = 2365204, upload-time = "2026-10-08T14:27:48.117Z" }, - { url = "https://files.pythonhosted.org/packages/ca/a5/458c4a29f52fdecb16b192fb27f117359da9c9c58401ebfd956637822dca/pydantic_core-2.50.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed557fa2744617eac3e34dd39b85037efbf23cd33f06851c00fdb8f18ad8f4c2", size = 2445007, upload-time = "2026-10-08T14:27:50.637Z" }, - { url = "https://files.pythonhosted.org/packages/98/14/0c0e72e0663be91a456167a2f7ed49c65195b388e8f1e8b2bb5083b27458/pydantic_core-2.50.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59f816dc04e99627a5a6352ae51dfb30e603f2cb0b7009c91dc640c533def014", size = 2159911, upload-time = "2026-10-08T14:27:52.881Z" }, - { url = "https://files.pythonhosted.org/packages/62/7f/64af6921e17ed04dda0e07ebbd394b29fb557d33618cb0f3f81ec59c0508/pydantic_core-2.50.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:779b6c74526596a86d38248dedaecfb7851bbaf319c234be042c57acddd2c8c4", size = 2235761, upload-time = "2026-10-08T14:27:55.194Z" }, - { url = "https://files.pythonhosted.org/packages/82/11/b6ff9f7207af629094f4deac947d04be3812d5c95054010ad8f710a052f0/pydantic_core-2.50.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:21b62d45f327eb0802f842c132fda6d01a8376a8077922dc4dda69011c64d34a", size = 2243726, upload-time = "2026-10-08T14:27:57.336Z" }, - { url = "https://files.pythonhosted.org/packages/8e/37/6f7101f3c746a1686987a32875ae2f7c19f518d783979cee1a8a32e0a9ef/pydantic_core-2.50.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:4f31af62efd1fd0257735b72e6716b32d4f207654adeabfe524d44baf1bb6bed", size = 2277158, upload-time = "2026-10-08T14:27:59.714Z" }, - { url = "https://files.pythonhosted.org/packages/d2/74/67fe208f3ec7f5d8c47bf7cba70019a84b1cffad947ebc8caef14271b54f/pydantic_core-2.50.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:933f2d639eb81a3e1f145aec415453cc00983236629933f028d9507222583a2e", size = 2450059, upload-time = "2026-10-08T14:28:02.17Z" }, - { url = "https://files.pythonhosted.org/packages/cd/10/4a9c56a69f5841bdd94ffff5876a12ad2733209d5b89cadb67638d69e7c7/pydantic_core-2.50.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:c05d035e72530f6b00297941b0162218601542b76870c3bf2756bd87f16fc538", size = 2479142, upload-time = "2026-10-08T14:28:04.494Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c1/30d36746051e42bb67df986ee874f787526edb99205f6b1583c33dd81202/pydantic_core-2.50.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ab3f95f737fc1b258b8210308fc02ce1442cf5950cf6d30b09ad89ab9e8afebd", size = 1483929, upload-time = "2026-10-08T14:28:06.527Z" }, - { url = "https://files.pythonhosted.org/packages/04/2c/c0e8949be4f99a74f02ae02d603bdd518c8a9eba72982a34d675134a792a/pydantic_core-2.50.0-cp314-cp314-win32.whl", hash = "sha256:f12d9690634414fc04b1a7072fdc35c34a9242232c1851fe4518383578bb09d4", size = 2104255, upload-time = "2026-10-08T14:28:08.729Z" }, - { url = "https://files.pythonhosted.org/packages/ea/6f/a5a5baf99509c998e0f2335d66b815880c7d6c63231c8e4017b237d6837d/pydantic_core-2.50.0-cp314-cp314-win_amd64.whl", hash = "sha256:63263d64884688554fb025a3906c7b00573980cfee75f9afeb86239d577384bc", size = 2170939, upload-time = "2026-10-08T14:28:11.08Z" }, - { url = "https://files.pythonhosted.org/packages/40/bc/c89b93b69d59cfe61c1e91b1cd3d87cc740e57327ecfef84ce34b7dcadac/pydantic_core-2.50.0-cp314-cp314-win_arm64.whl", hash = "sha256:753863dd4317ec8cc9eb3e6d9d01a8ef1a1726a8003b4354658d68a1ae05f9db", size = 2157035, upload-time = "2026-10-08T14:28:13.445Z" }, - { url = "https://files.pythonhosted.org/packages/ff/5e/d0ad47a406c42db95876900f4bfc520313c28d07a0dd5080da4972220c27/pydantic_core-2.50.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:c4abc425789f8540e86ca4cfdbc8dc650433cc2ac7c6136fee9bcb29d5665a02", size = 2234007, upload-time = "2026-10-08T14:28:15.9Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f4/69a7ec8400c1e34e5e11ab967f850ffd8ca71bb3aa0a8db93df5912bb6f2/pydantic_core-2.50.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cf150693a51ca21e8288cd08a9de05e5ab331776dfcfd0537b14523338f0502a", size = 2000888, upload-time = "2026-10-08T14:28:17.983Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f1/860bb499f7cdf4bb453e8080af5bc182487f2f025d94baeee29c907bde6f/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:158749408ee19682b8a2a7e7135cced7f41d6f2f9de96088b1b1609858a6a158", size = 2022761, upload-time = "2026-10-08T14:28:20.197Z" }, - { url = "https://files.pythonhosted.org/packages/89/fd/f571420436e79b9f7cc8f8ec534dee1759c240b4dc086737608f14d05c93/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dcbd1fe5083315243447c13f7252ae9fe9d128b1ae2857e3a916c609235dd863", size = 2192910, upload-time = "2026-10-08T14:28:22.502Z" }, - { url = "https://files.pythonhosted.org/packages/cc/94/b47c4a01ea978a7a3d02551e3625f0344ae1352d67ea78011c2eca5e39e4/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fdeda6272d60b1f6fb63f6a3dade55e274af62e1514d24a6549a12150c385cc", size = 2390178, upload-time = "2026-10-08T14:28:24.861Z" }, - { url = "https://files.pythonhosted.org/packages/77/61/d109f26b3ee4443cca220eaaa4f3fbcf78ff07aeb577becc37d9224db63f/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b16e164205a90b1050d2f5469f8a7829db7698ad198c69e6aab6cbfb5648b87", size = 2464453, upload-time = "2026-10-08T14:28:27.383Z" }, - { url = "https://files.pythonhosted.org/packages/de/a4/7be2f608f6f8e7655155057a82a7e2f135ad9b37fcbadd34fdf11b1ade24/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17a5ca9c197788a6424749a09a3dce824cb2c17b72f835f8a5e330935b973609", size = 2150682, upload-time = "2026-10-08T14:28:29.65Z" }, - { url = "https://files.pythonhosted.org/packages/80/ef/a8d867f6d3981c232d0f3c2254458cdee8471de7bff3f7edd252cc00c30c/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:01340f4fbb4f854b1f36a6fe9dcd2b26c8936aead4e4ad1205624ac025c875fc", size = 2245995, upload-time = "2026-10-08T14:28:31.934Z" }, - { url = "https://files.pythonhosted.org/packages/9f/a6/7297a39c8814beab877be5ba4f594c2e166108cdd20c1f1c51fb71d5389e/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:99e203d5c2a814facef78dcd993b0fb7a933124a3475a38979fc09cceae210b7", size = 2264090, upload-time = "2026-10-08T14:28:34.298Z" }, - { url = "https://files.pythonhosted.org/packages/c3/65/634fc407eaf61abec0d015d6fd4fd8456c18c71358e984f60b0a05092d49/pydantic_core-2.50.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:2092ce156f92aff17e3345baba2b7c0c1701f32ba922c5de71bd6248fbe164e3", size = 2293708, upload-time = "2026-10-08T14:28:36.725Z" }, - { url = "https://files.pythonhosted.org/packages/3a/04/b87fcf8062b8907c77769385a4340a814fe359660593dbbe4d8f3f7e9e61/pydantic_core-2.50.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:74cbb6cbd74445ca279668790e0c10eccac0428fd79fe061fce6c9e3982ad3fe", size = 2468464, upload-time = "2026-10-08T14:28:39.109Z" }, - { url = "https://files.pythonhosted.org/packages/77/f4/d7aacec95f9e00dcbad84bee2fd0fc081ae292d380dc8a3268e4103de957/pydantic_core-2.50.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:42fff617cb0b08505d8123d71e6e7a8e54210d9007498f564855bd843ce984b1", size = 2511897, upload-time = "2026-10-08T14:28:41.681Z" }, - { url = "https://files.pythonhosted.org/packages/a0/d8/0a755b0069f9a0f4c57f98fd05b524473952556406244649d632d25f8646/pydantic_core-2.50.0-cp314-cp314t-win32.whl", hash = "sha256:36f9ed6ae1069913e4f6e86d8233e119e83e00a20c54f88faf9c81292f2fecc0", size = 2098906, upload-time = "2026-10-08T14:28:44.218Z" }, - { url = "https://files.pythonhosted.org/packages/27/4e/cd10a1fbd1ba1d730871e382ec2ac550487465c7c9be1a72b3f4f9181817/pydantic_core-2.50.0-cp314-cp314t-win_amd64.whl", hash = "sha256:980c81c2ec53ea9eb2227c14b3e6de35670b2de163b638f2a803e90a3bd5bbb0", size = 2148359, upload-time = "2026-10-08T14:28:46.47Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b1/a766eadfbc16b58d401f63dde3bf4e0943dcd2402011b6b8d9de2031ebe0/pydantic_core-2.50.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dec0dafc116ac29a84d143fdbc3b83fdb5d4ed339276be2251154537ab30e14d", size = 2175241, upload-time = "2026-10-08T14:28:48.934Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d8/43d0e765a80d6fc7d87b5f66a9023777aad6da7e7d64f486a4c2be8158b7/pydantic_core-2.50.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:af2b808a79bb04075e87c81a5b6179365b93f9a851f29dafd67abff72085d0c8", size = 2224750, upload-time = "2026-10-08T14:28:51.227Z" }, - { url = "https://files.pythonhosted.org/packages/9c/12/07e047c21ad90f184c7e8bfc6c9966b9ca51d4698e1a06460b65ca2ce105/pydantic_core-2.50.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2918547195ffb20118b829fdb8938e9dc92c9527fe6cbe58572594c96362880e", size = 2023936, upload-time = "2026-10-08T14:28:53.978Z" }, - { url = "https://files.pythonhosted.org/packages/d8/a9/fb26fdd343ead65a699245c9d2150e84c47496a9b250859214a90cceecc9/pydantic_core-2.50.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0393763d66f6f61715488d074a2cefac04aeb3ee281e36fb4925dd44deaf9e17", size = 2048872, upload-time = "2026-10-08T14:28:56.342Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4d/9fc4ea28a8ecc1b85fe77a616f8df9a51696476aaa93c5b256269d025044/pydantic_core-2.50.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62ec6568896e0abf258cbcd22c406c1c8bf27d16224b21bec8f75c4ae88a8173", size = 2173752, upload-time = "2026-10-08T14:28:58.805Z" }, - { url = "https://files.pythonhosted.org/packages/70/7e/74b55196339413d787b283c2568cca4679b0645e48229421228acfa8839f/pydantic_core-2.50.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec786cb9d597dd75d993f8c1273e31bb2114c9bc22f67fba611e654e8347701b", size = 2365284, upload-time = "2026-10-08T14:29:01.434Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0e/4ebc7a851f0ef63fcaba205739c171e50dd183f8d79dcf0099c3b46aa60b/pydantic_core-2.50.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fce74add1099da1ea09473268950270071fd56773e2968604efb3ab1d240e02", size = 2445279, upload-time = "2026-10-08T14:29:04.034Z" }, - { url = "https://files.pythonhosted.org/packages/07/09/03d3524fc7d4960840e32fd88c8cf487b5516daa7b6845ffc74274e0247a/pydantic_core-2.50.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:243088c95e23b12db9f2cd7661d584a3f00814e087489f40cde7f9feac56b694", size = 2160424, upload-time = "2026-10-08T14:29:06.628Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b9/7c530c84049089033dd746700bfbb51520ed9376bc90f3dbbcb319880acf/pydantic_core-2.50.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:42a56b0052ac11d9d0d87b1c94a1ce52e31914fd133f269585e0f63a4ed988f2", size = 2236109, upload-time = "2026-10-08T14:29:09.053Z" }, - { url = "https://files.pythonhosted.org/packages/0a/08/70e07379ebc2538c22f8c372a37c009b96a3a1bd7921aee1dad28ee41be0/pydantic_core-2.50.0-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:350001f5573451150d919722ee095aa28bec037d6e23b86d7f04581a910fe924", size = 2244011, upload-time = "2026-10-08T14:29:11.549Z" }, - { url = "https://files.pythonhosted.org/packages/66/49/294810baacae4b088bfac60edab06036951e5ae6e0c20e8f6cfef7d70f66/pydantic_core-2.50.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:21e38a011783d8afc8b9d79928e273d06f349b52ae84edf63ec18ad07f077484", size = 2278392, upload-time = "2026-10-08T14:29:13.998Z" }, - { url = "https://files.pythonhosted.org/packages/c3/0e/6990b812f124cf3564b36492799df7a66cd3cf5678004ffe65dda85b136b/pydantic_core-2.50.0-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:588d309ce5c85448379556d72011b191c0414ee2e80d7c3f1ebe2ceb2d9027b1", size = 2450012, upload-time = "2026-10-08T14:29:16.524Z" }, - { url = "https://files.pythonhosted.org/packages/a8/c4/f8f1a763550a077c51d2c69b7a32db56d1b54e61c613bc19e6c7fd2e7cb2/pydantic_core-2.50.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:a8ee3965e01f10e4ff92ba1727626328ab7ba93bcd5674ff2b78ea1048ee0cea", size = 2479233, upload-time = "2026-10-08T14:29:19.111Z" }, - { url = "https://files.pythonhosted.org/packages/9d/83/7a09baa1e0e4710be47b84aaf808e7b86247d7fa43251ac8934099113f8d/pydantic_core-2.50.0-cp315-cp315-win32.whl", hash = "sha256:c05b75ef3574c9ee4e05bbcf8513f7ccb155d426514be9efef5f6f53152d5f5c", size = 2104507, upload-time = "2026-10-08T14:29:21.694Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a9/f1cf61f747538834ea2c14c442264e4f29663c95294ad7933867633a1dea/pydantic_core-2.50.0-cp315-cp315-win_amd64.whl", hash = "sha256:8447678b49412294801c9ea15ae31ea3bae7da65425268c92d40e032c38eac6f", size = 2171379, upload-time = "2026-10-08T14:29:24.613Z" }, - { url = "https://files.pythonhosted.org/packages/30/c8/9871261b760672cfa334498d94bc7dcbff2495345e639e806127f7087254/pydantic_core-2.50.0-cp315-cp315-win_arm64.whl", hash = "sha256:92016718bcf3e6f35a6bd986880191a8da7a35aa1f5b1e97544582ef938464cf", size = 2157182, upload-time = "2026-10-08T14:29:27.082Z" }, - { url = "https://files.pythonhosted.org/packages/de/3c/5107269aee5ee7855fa370ff837998792dbf124b8545244c370bec251a66/pydantic_core-2.50.0-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:71061800a3c225e730f7997f8a7fad6e0d0dcbe36609cdc7e60576a099a2832b", size = 2234440, upload-time = "2026-10-08T14:29:29.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ac/cb179b0c404337ef517515539f9899164a6d3d43d5c79c2556165da211c1/pydantic_core-2.50.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bc87bd34239835c8d73171acd039e591cea0a2ca8615e6188044ad170a40fca1", size = 2001074, upload-time = "2026-10-08T14:29:32.327Z" }, - { url = "https://files.pythonhosted.org/packages/b0/59/ef1714204f145e9497ab71e8bd82c5e40b5adf365d420be777d1e047cdaf/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57c0e5b26d82bf31ab2b044781527b1b1a36e9ed400856b6aca5097eb1abb909", size = 2023016, upload-time = "2026-10-08T14:29:35.06Z" }, - { url = "https://files.pythonhosted.org/packages/87/e9/08a3ade34b4af09a83740b62a4442b7623127d388fc21d4938798dd85a26/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:41b9f2821f0a105f88cd54ad03fe1392ec600618ffa8610f7c5e466ecd98c531", size = 2193165, upload-time = "2026-10-08T14:29:37.721Z" }, - { url = "https://files.pythonhosted.org/packages/2a/56/29aa3e540d3aca72139e5b7dc56395a940849d85da2b7a9ccfbff71d0928/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:364f62d8024997536e57865cb1c8b36effad3249bf392a29a2379ea69db28238", size = 2390198, upload-time = "2026-10-08T14:29:40.616Z" }, - { url = "https://files.pythonhosted.org/packages/76/63/a5cbdde0a2090c47002819d85317428e15b41d22e581cf38e9714b4903af/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:955d7878130dfc124d6a5343e1b87d2933244fe14a4a0a3e98787e8e660a8eb4", size = 2464777, upload-time = "2026-10-08T14:29:43.29Z" }, - { url = "https://files.pythonhosted.org/packages/c5/55/fff3a363b0dcf72fdd72311c676b1fa769f2c13342129c694b2eb9886580/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf81432281af66ba3285b26b09c2d478d84730dc50ff90926c1bdcef54048a33", size = 2150389, upload-time = "2026-10-08T14:29:46.271Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f3/0062361fd2377185d322dc84cd05ec4b96da352195c89939c7b509664721/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:34a4a0938eb30931baca56e55786c5a6871ac7aa891a38c8cabcdb7e49dab91b", size = 2246225, upload-time = "2026-10-08T14:29:49.173Z" }, - { url = "https://files.pythonhosted.org/packages/f9/45/662e3870143d5f1632a02ceb9ce2d5168e88caf3da80e9b42f495ceb1284/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f517a417cd02aa8fb05b603a0ac6d87b7b004c3ba4fcd279cad25cab7229043b", size = 2264404, upload-time = "2026-10-08T14:29:51.699Z" }, - { url = "https://files.pythonhosted.org/packages/32/5e/8b14ebf111700362c6e519e40209d86f521da81ef171b2891af0bb5de311/pydantic_core-2.50.0-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:d01029d54ff1f45c195b1f7e6fbf6e58fb7e7e12cda9a6e639570d9c581decb2", size = 2294152, upload-time = "2026-10-08T14:29:54.349Z" }, - { url = "https://files.pythonhosted.org/packages/55/29/85c486e0d25a8b523803032e97d7138edb7b7bc887fa8c6dd085e8824cdb/pydantic_core-2.50.0-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:d06dbbfe8da01a0574de27afc19915bb3e7dddfbd184bb97e958f94051d8531b", size = 2469160, upload-time = "2026-10-08T14:29:57.193Z" }, - { url = "https://files.pythonhosted.org/packages/12/bf/c451db7567e92d6601c4d2f6bdbb945cf60104182e2b241d8fe0b6f01414/pydantic_core-2.50.0-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:5307a8bd49158b57a0ca4e10950d31085aa35d9007e934047043ccc6d28eea97", size = 2512204, upload-time = "2026-10-08T14:30:00.177Z" }, - { url = "https://files.pythonhosted.org/packages/43/1e/5b93a2513ced099acb0dc4dd6684742d6564ade60a59abcbabd161624e07/pydantic_core-2.50.0-cp315-cp315t-win32.whl", hash = "sha256:c2b246fa7cbdf9918488d1542a82bbb928cf71bcba66905c24131981e759ff0b", size = 2099026, upload-time = "2026-10-08T14:30:03.154Z" }, - { url = "https://files.pythonhosted.org/packages/37/f5/1b5967e31b025a23f2f627ffea1f5a874cff0adb72fa5f5af6bb36394ad9/pydantic_core-2.50.0-cp315-cp315t-win_amd64.whl", hash = "sha256:36c49d4e1769127461b609f110d91963790091ffcc2de401ac1d3b2f6a63bd54", size = 2148456, upload-time = "2026-10-08T14:30:05.971Z" }, - { url = "https://files.pythonhosted.org/packages/48/15/213d6fe84816e8a5ed7b7539ecebb3f14c20739c2469f4e372f0f15bb042/pydantic_core-2.50.0-cp315-cp315t-win_arm64.whl", hash = "sha256:f3abcabb04503023e1053add24472e78878de0bfd5c8a93686be01f4032c363c", size = 2175368, upload-time = "2026-10-08T14:30:08.989Z" }, - { url = "https://files.pythonhosted.org/packages/11/ff/8d126ca04417cbc76a81648c135296ecc2f36d07adfb0b3376638efd1dd0/pydantic_core-2.50.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:38b03c5439c6a7a952f56e3e6596dae44bc78c3fb6a69df46cabfb2f888e5f0c", size = 2162541, upload-time = "2026-10-08T14:30:12.075Z" }, - { url = "https://files.pythonhosted.org/packages/b9/3f/e3f11c40950d0f37b3891c1ed015f04a4ef36c881d04ef1a5d1ac91d1c6f/pydantic_core-2.50.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:4faa766450ef44d9eabed5b65a280f63f903d1e1ed6d2e278961eb22345ec49f", size = 2075402, upload-time = "2026-10-08T14:30:14.968Z" }, - { url = "https://files.pythonhosted.org/packages/42/f2/727d2b23ff6ca67c9c050c4ff4e5abd7b599d9ebcd819a74141d3b11c7c1/pydantic_core-2.50.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50920d66aaab60dbc6023c3035d55fbabfa58b6d205d0b39d47d820c4a7a13a8", size = 2108552, upload-time = "2026-10-08T14:30:17.774Z" }, - { url = "https://files.pythonhosted.org/packages/4a/da/a4321058eea61f0504366668a09472a4c37514be30cce77967c35ab7ffaa/pydantic_core-2.50.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586699b43066ca6038f96bd71d0eff0b6c292929e3d66ff63d5dc79d0f06d589", size = 2267745, upload-time = "2026-10-08T14:30:20.619Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c8/45e2cbac6379b8cec8cffd7e6aed50754fce13d93f0cff28bbbfe18c1d1c/pydantic_core-2.50.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b916ff828d604d4311a5639b7b3da51eaea3923833ec3e300a5ee35eade99691", size = 2217049, upload-time = "2026-10-08T14:30:23.532Z" }, - { url = "https://files.pythonhosted.org/packages/61/8c/b81c4139bff6305e7a98fa7845a99e05df1461d3c33d0fef417e02114ae7/pydantic_core-2.50.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:732efbb50977ab7cf18f01d4c255834aeb14cb2425459f7bff681a1ba3a4ffa1", size = 2039121, upload-time = "2026-10-08T14:30:26.423Z" }, - { url = "https://files.pythonhosted.org/packages/0f/49/679293741809cf835290bccf80216aebfcf5d94cb84a34709f18b21abfd5/pydantic_core-2.50.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad9185366893714cd4514ae5e1a098227704fa395cb41a7855d75968ecedc826", size = 2109280, upload-time = "2026-10-08T14:30:29.216Z" }, - { url = "https://files.pythonhosted.org/packages/f5/49/76f167aec9b9d77a82e38131539ca6c6fcf69d720965f76eda20b87b454d/pydantic_core-2.50.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0def1dc09802a790e4a1b3cbc4401f0b53c58f273ce3671df9867f7bdf1fbe20", size = 2269313, upload-time = "2026-10-08T14:30:32.096Z" }, - { url = "https://files.pythonhosted.org/packages/1f/50/a6bd3c397b609aade207ffb78ce46f4ad4e29bb42a39007c87ee00e778f3/pydantic_core-2.50.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:68421ba548f6d86c7dededd70bfe530b4faf67eb811c53c70a1be69438d3ce99", size = 2154371, upload-time = "2026-10-08T14:30:34.965Z" }, - { url = "https://files.pythonhosted.org/packages/11/3c/2d32a1f945b9809b65701d2b72315bdcd700f13c093df5e60baaa784bc6a/pydantic_core-2.50.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:23e0940b7d73f405d92e98b2a05b93b16db163be13b7478bb19e1db2e486ade6", size = 2087028, upload-time = "2026-10-08T14:30:37.814Z" }, - { url = "https://files.pythonhosted.org/packages/aa/57/c2aea271192d8c4b81afeea9d61de8f5dc8686a1fcd1ace80e70a482fbce/pydantic_core-2.50.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee9913fa2b5bfa6111c11158cf481bb13544f8bd5d10382fb6ee2612b16fcf28", size = 2259611, upload-time = "2026-10-08T14:30:40.581Z" }, - { url = "https://files.pythonhosted.org/packages/35/88/32128d5498b95722fe914a21a25f59edd7dc8917474f47924005682532a8/pydantic_core-2.50.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ad3532291deedfdfcad3cf5d351d0076d646c68a0c63869a1bebf87c22159600", size = 2268636, upload-time = "2026-10-08T14:30:43.511Z" }, - { url = "https://files.pythonhosted.org/packages/8e/37/7dae31279e07aaa63dd4b17756f4ebd5647aed6bee5581ee3b3e85d82a0c/pydantic_core-2.50.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:9279f4be22bc3765dd9f61f5b4d38d9cb219d167bbd6f2e0ffa3368b1b71b4d1", size = 2305416, upload-time = "2026-10-08T14:30:46.386Z" }, - { url = "https://files.pythonhosted.org/packages/10/2a/61e47defbd490618fcde2641f80d5c077f2961de62e88500edacb49297f9/pydantic_core-2.50.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3b2f3e44af4c6512dd73557621385a71b18094c8be81d60e5a8e73a8ce96b7f4", size = 2466713, upload-time = "2026-10-08T14:30:49.214Z" }, - { url = "https://files.pythonhosted.org/packages/61/57/81327752c007834d684b0b7d20ce00087206f512c7347a6236fd89ef0223/pydantic_core-2.50.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:593dd4f24abeb3ed90222f98bc6eefcac68cd5aa96fc7745f93862a802952bce", size = 2481971, upload-time = "2026-10-08T14:30:52.088Z" }, - { url = "https://files.pythonhosted.org/packages/48/c3/e321d3a4b2737372cf775ffdd1a02428c7d2791ee5afc9f228793a0ee245/pydantic_core-2.50.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b123f9d8702106f39dc3af63a031ca8b5279862a3747ec6c03ca49dbe78b71b9", size = 2337958, upload-time = "2026-10-08T14:30:55.045Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/a6/24/af4ec4be49fbc810f35b0bdcacc3d433b56bbfa469fd3bfd4ae116cd9bf1/pydantic_core-2.50.1.tar.gz", hash = "sha256:e50d7b94baac6c7d09927fa5ca5800a0c7ee5015c7fcff65beb3a1931b5a6e09", size = 513515, upload-time = "2026-10-11T18:35:44.82Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/1b/ca81811da3d2aea724fb3c4ba6b83f50ab8f210576306ccd601973142d35/pydantic_core-2.50.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c531166c42ea7bdfecc8c50049581f05dd1993b09cc7c52bb36a14e96deaec7d", size = 2152145, upload-time = "2026-10-11T18:31:58.046Z" }, + { url = "https://files.pythonhosted.org/packages/59/20/938ef7e0541af0a314e2f9d63848dbd1656ba2ebfe5ca0469f7ebee00d5e/pydantic_core-2.50.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6d0c2183008c188e19f4906d426b293bdc4f67ab17df8e180fe16cda208fa71", size = 2045761, upload-time = "2026-10-11T18:31:59.35Z" }, + { url = "https://files.pythonhosted.org/packages/2f/4b/456497fe2affa18f49b226e89bde31e8a204751ee343dd9beaac5438123e/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94be440c03fede26969a5ce75468e0e6a9927a1b46d9b679ee8adc1b057b0350", size = 2049557, upload-time = "2026-10-11T18:32:01.121Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/cd5dfe13928f55e236dff3a1bee06721d318ca6c955d9ca993b0a5d09f39/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:36c426eac0af8d1529ff8467e612b933346caec1fdc0d774f78f67a1a11e16c1", size = 2188153, upload-time = "2026-10-11T18:32:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8d/d0b25bc484f8d9136ea76ece69e0b2d24ab370cc2e7bbd1b79e61723b763/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:028e2f212273d4a39b1ec1e0de8166b1165a65fc0f1111452a9d94fc7c625c63", size = 2388308, upload-time = "2026-10-11T18:32:03.767Z" }, + { url = "https://files.pythonhosted.org/packages/c5/64/6513cac7c9f78492abafa01d57a69de1d246bf3085fd7b0330bf14b025b3/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6f0cc1bb9900dc558960894adeb30b0c083366fc1d69b856209fb2ca5c36fe5", size = 2431187, upload-time = "2026-10-11T18:32:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/ed/60/7f2ae89c37e69a3dc0da9ff57b0388c285542c688d05f36767770429de85/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8812592c85d0edf423f10eadcef42716d71e8219085ad9e85b775057b7306133", size = 2157623, upload-time = "2026-10-11T18:32:06.616Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/e3a71373b85a7973099b9a229a4302c7792262717e0b4997e41c01f7dd16/pydantic_core-2.50.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:bbce99252ba3167b2b6277f1829d5bf4b43b754524bddf7f944707c3db7d2253", size = 2254539, upload-time = "2026-10-11T18:32:07.968Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d5/5b620aaf40dc631d911565b64d49d3fa8422027c046a7a96dc4e4921ae98/pydantic_core-2.50.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:476f6ed8e43cd1e0b460920e23571700872b284e77331cb30c4faf459cf48a4b", size = 2267344, upload-time = "2026-10-11T18:32:09.399Z" }, + { url = "https://files.pythonhosted.org/packages/e0/48/f46baf4580d8a56ca4782aa65842e16d3cf39736b29fcce8e9c0521b3422/pydantic_core-2.50.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2cbd1b75b09e976ed0d6b6ca297675632ca35df86130088457cdc60ef36970ae", size = 2300877, upload-time = "2026-10-11T18:32:10.903Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9d/e7d28505e6092c1fe075838a4f793ccbe6dd676e44ab16175815ee06cfe0/pydantic_core-2.50.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:5958c72adb417c39b12ac87525ac60b0d73315fcdc59e21f44ee4a5e2512c9ef", size = 2464270, upload-time = "2026-10-11T18:32:12.329Z" }, + { url = "https://files.pythonhosted.org/packages/22/b1/3bf63b82efd93ebc9968f9c12fc46037a28bcb452c88e4688f33e5ff7dca/pydantic_core-2.50.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d8f9e8a6c4ab04b78d61f78627370d834eb004b2869dcb28cfffa647b4ea1980", size = 2478653, upload-time = "2026-10-11T18:32:13.735Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/26d1e4b9c2ba6a92b7dc75bbcb99ef44456b7e3af1bced868403667ea7d3/pydantic_core-2.50.1-cp311-cp311-win32.whl", hash = "sha256:4be846f55c9477f5f3ddde8f2ce941137e16862a56d018ed885d422bb6ae02f2", size = 2120246, upload-time = "2026-10-11T18:32:15.591Z" }, + { url = "https://files.pythonhosted.org/packages/24/65/3e875e7991eaecd4d70686293ff0d8a99aa8f93dc295e6a21c3d702912fb/pydantic_core-2.50.1-cp311-cp311-win_amd64.whl", hash = "sha256:0048b6dddc8ef4b64fccaad878bd143b0c3882ea9936279dc11d613f6b7dd1bc", size = 2176332, upload-time = "2026-10-11T18:32:17.153Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5c/58dbd17c61f209d91c70f549eb7a3bbec7023e89ea12dead091462d6d658/pydantic_core-2.50.1-cp311-cp311-win_arm64.whl", hash = "sha256:6a733778df2f7087ec1100ed0b41533e4f3001976e99570fa34f57c66e7f8e3e", size = 2182471, upload-time = "2026-10-11T18:32:18.49Z" }, + { url = "https://files.pythonhosted.org/packages/ee/94/101b49a6c63f99586755690696eae14532772587d9e39e93f7b8d28a49da/pydantic_core-2.50.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:704075d10b74f2f3c6e15407c696d88701df35fc8953f434a431add0d0074db0", size = 2222428, upload-time = "2026-10-11T18:32:20.228Z" }, + { url = "https://files.pythonhosted.org/packages/db/a3/f8b1b39349c33037c73d7a1b794c9c29d8f07d5bd2b74399c1375a529cc6/pydantic_core-2.50.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e8e1d6ce820aa23317e8209a86bd65a540973c12dc7552b48a4f6c8e9926815e", size = 2025779, upload-time = "2026-10-11T18:32:21.723Z" }, + { url = "https://files.pythonhosted.org/packages/d2/2d/862a7d115305cc621358e51ba13f296e0ffdf4fbd984be9f63bc7862730b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c18db21573bd2c6489f9a544b7499f0df2853958c568e5e783536ee1f690af41", size = 2048429, upload-time = "2026-10-11T18:32:23.3Z" }, + { url = "https://files.pythonhosted.org/packages/c0/7d/fed95d18bf2b5abe2d5ab2bbec4184bfc22b6805994a33ccdae8edffd25a/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb57f304525a5e3c13333b772bf9a473f36326e9c821b2e8e1b2fd36f80ae2c3", size = 2173274, upload-time = "2026-10-11T18:32:24.697Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/c6f22ceac65dc3d291ff79bea0eb74cbd1de8b128f01fee48af03bcea1e1/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a27c09d86600f1bf2fe3f37e1ae697faf3143931c09322cd799da94deee923b5", size = 2360403, upload-time = "2026-10-11T18:32:26.281Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/190b5b6bfe4c72f265feb657913eb30749193927a5ad7b83cfa7866a25a2/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46b3301d3b5c886f77de7546e47274a5842c622ea2020b8c6524c6b66913b4a6", size = 2439799, upload-time = "2026-10-11T18:32:27.844Z" }, + { url = "https://files.pythonhosted.org/packages/90/24/48cd98388e4ac2c08d0800af36d15413db15e20387f1038123a2c86cb2f6/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ba4e9d8210d941c200431a56b2c0400b131865947903937ed3ec5404307d2e", size = 2160157, upload-time = "2026-10-11T18:32:29.553Z" }, + { url = "https://files.pythonhosted.org/packages/00/86/d909133d0b9979ed14c20f110f21124a0e346caf071bd66ac5af3009062b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e5faeaee74a57d32b3ab3aebad2e348f06d3ba946fc5d28c1728455f00a3d13a", size = 2229523, upload-time = "2026-10-11T18:32:31.027Z" }, + { url = "https://files.pythonhosted.org/packages/90/31/4b43f08131b2098e73a852fd932063385f3103b33839fd9d1abdb89e673d/pydantic_core-2.50.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3cda0e538208e5d722bbf3698b24f19c0a7d05bc8d5f8a7f9b121ea7fa243d9", size = 2242700, upload-time = "2026-10-11T18:32:32.622Z" }, + { url = "https://files.pythonhosted.org/packages/88/77/de39658569dc7a4c293d110a89629f61145e7538f00d1e4280c28debefd3/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:57f51b31ff826e2859120cf4737c5a758a48d96f3e97da40ccee1796d58078ff", size = 2278452, upload-time = "2026-10-11T18:32:34.165Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/1eeb9e4e791e81210ef9096f58080b32ba15b5b13c108d50aaa3f8ce9f73/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8daa7ee75245d43ad7d747e5c9ecc1b1d06552f72b14887e9276f787d57375f4", size = 2450098, upload-time = "2026-10-11T18:32:35.547Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/b2accfdff17e5a88d6358536702146de00f9339adaa4d5bbe1ee93cb4832/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:acbf31f37c53a5ac0c34706c80b4f5107ba20b05fdd3816124bf236ef0c57dd2", size = 2476839, upload-time = "2026-10-11T18:32:37.119Z" }, + { url = "https://files.pythonhosted.org/packages/3b/7e/c8a83eb5d2cd42c2e13507150fb777b1edea5a018adc9cbf2858a1d08285/pydantic_core-2.50.1-cp312-cp312-win32.whl", hash = "sha256:45b11cac094aa25725581d9304eee93c9028516b9ea80dd9e175e13a5a2c840e", size = 2101425, upload-time = "2026-10-11T18:32:38.593Z" }, + { url = "https://files.pythonhosted.org/packages/c5/17/8bbd530b8659e9d963f5b16f6cb8e159f4cd174b4d6304e48969ab12dba8/pydantic_core-2.50.1-cp312-cp312-win_amd64.whl", hash = "sha256:132529c83901437ff642f585216831bf5fd7a91df66829907e155192ead62498", size = 2172522, upload-time = "2026-10-11T18:32:40.112Z" }, + { url = "https://files.pythonhosted.org/packages/af/9f/a79d690b127e4e3cff143d031538ab9aa715f6b2fbeecbd07cd6f2bf9fb7/pydantic_core-2.50.1-cp312-cp312-win_arm64.whl", hash = "sha256:4e834f6a8e4ff772dcc34f58ef5504147a3ea5b0f4eeb13b0f8eb2ca75ac57f1", size = 2161366, upload-time = "2026-10-11T18:32:41.606Z" }, + { url = "https://files.pythonhosted.org/packages/ae/57/0e237d7091d2cd44a35d243b7344227f90440166860469cd036afc23a04d/pydantic_core-2.50.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d5e062c01286d861fd6a1c4ff6e063547b3e713067f2df033c0ff97ac2ca006b", size = 2222774, upload-time = "2026-10-11T18:32:43.103Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b9/c720e56858d4e1539503297ed37063e0c08e0f3541c41b777f6a800f75fa/pydantic_core-2.50.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c003c3b7f49debb893d2d85ae099ac5959c9839e2f330fadb1fcdf7a6594482", size = 2024738, upload-time = "2026-10-11T18:32:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b8/fbfc25875219cc060e613170ff10e850c6d8924beb4910da57c2ee3ba1d2/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:409e0ea40ec30d9158f33574fd758e689f6045a0f2596701828c27816ca9687d", size = 2047049, upload-time = "2026-10-11T18:32:46.164Z" }, + { url = "https://files.pythonhosted.org/packages/d7/43/34210124d504c553688f2f04b48500b131237528ac545b445e0d6d30e0d5/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:131059670f1d2444269b8585cb888963994871932447c08b39ac6a51fcfef658", size = 2173415, upload-time = "2026-10-11T18:32:47.722Z" }, + { url = "https://files.pythonhosted.org/packages/65/cf/6e178e8fdc11da5965bef980983bf46326a42f436871dd51ba0a57f39df1/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dbcbee53bf17196a7f745aa9bf5a9603953a1e365b1f020be3207c676a3e7c4", size = 2361719, upload-time = "2026-10-11T18:32:49.217Z" }, + { url = "https://files.pythonhosted.org/packages/11/14/bd5169d356aa91bf777e28ba0b281c192d286d03c2812c9c9db023a2f00e/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:325c23f3e35cfbf0fe3486fa5f7260d1e45885173002d30a28ca019994124255", size = 2439803, upload-time = "2026-10-11T18:32:51.025Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bc/d79d000e5203ebef39af839f2ce77a777fcad6e08ad26af9a2fcd114ffc8/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17e722e156d0444ecaefbe640bdb60928752bf2013e2b7a11cdb099aaae19bec", size = 2158402, upload-time = "2026-10-11T18:32:52.714Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/4902cb5fd599422c130bd3124ab31ee8b771199bd56662702eed07d3fec7/pydantic_core-2.50.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:aa8224f10880d9bf1b5993988ba153d42a8b4f3f4f511f93b1f09c93ff613c72", size = 2229755, upload-time = "2026-10-11T18:32:54.126Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f5/c1481f8669f6060d89110c9b1374173fc8767ca276b84f4870bd8acd5e3f/pydantic_core-2.50.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:41bc8237121bd8dc8d888dfd6279fc166ffc88c1f1bf3a8bf00869680533ca4c", size = 2242303, upload-time = "2026-10-11T18:32:55.641Z" }, + { url = "https://files.pythonhosted.org/packages/04/f9/77fc3c7653ba9b6e42049e17e96b25388187d4a7c274ab1cb07f58ac8419/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:45c6266d071c241f2a168d45bf8c54344f0effce35e7e6b73afdec11f3687568", size = 2278345, upload-time = "2026-10-11T18:32:57.38Z" }, + { url = "https://files.pythonhosted.org/packages/95/9b/0579c5d12e7f2b16b27e6782427987341fcce07b0725e02ddb0b74add0c4/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:1deeacb112d14d3f4fcb16b165f7dbaf76c70ba6e82f37ba042bdab51970a0b8", size = 2449947, upload-time = "2026-10-11T18:32:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ac/1b677db91eba54cc5922f4de6edc46ba45c2bd712dfdc0130382d158e214/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c96fd793b73d1b92e65570132505498fe7b21eaef73cdf74e67e5dfba7ac9e4", size = 2476889, upload-time = "2026-10-11T18:33:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/4d/2a/3a9f6624ee3ea9ccba5249dde11418bb4c35780a7a92608f8768bd3fea39/pydantic_core-2.50.1-cp313-cp313-win32.whl", hash = "sha256:06ead20d39ffd6f2f6f2a8f8a6de67ff8bb1b4f14a8a30e058502514ee2ac685", size = 2101118, upload-time = "2026-10-11T18:33:02.329Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1f/323f78ddd9d9938aac420c1abb4e8ba799fc8bdab0acc67c0593b837c979/pydantic_core-2.50.1-cp313-cp313-win_amd64.whl", hash = "sha256:7816e98acc08119dc0f340ab167048ecc54126316330c1f0caf7c6756c88e28f", size = 2170840, upload-time = "2026-10-11T18:33:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/cb/09/8497c52a739ae425c3ac2f7f56414cbc711c67d374346174f40fe2062644/pydantic_core-2.50.1-cp313-cp313-win_arm64.whl", hash = "sha256:c17799a62c142d61b8a3c51752a7cbc87fe2ad4ccfab10e628a77b405075c662", size = 2161633, upload-time = "2026-10-11T18:33:05.518Z" }, + { url = "https://files.pythonhosted.org/packages/49/33/28b96e81677153715e3eafb9f26663a80841e859fde282a380359b0d3fa1/pydantic_core-2.50.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1cf41f1ae3fa155cf167a72689ad044bcc1e3c97e064123677149bdfb5dafc4a", size = 2224428, upload-time = "2026-10-11T18:33:07.153Z" }, + { url = "https://files.pythonhosted.org/packages/94/40/15c06410c9b7b8da5805d27b64e09bd3f900e986728106db2689dbc51513/pydantic_core-2.50.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4df197990c15b5a37c5a277d131d9f2c67de6133f2e5dafd80d9bba4b99f46f9", size = 2023740, upload-time = "2026-10-11T18:33:08.763Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4e/5eb629f6efc2a27e421d789dd4bfacbb5f6d09c80c28b13d1e87d73163d5/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0036473f5583e6a60e50b8b21651511564277a3f05cc5dab8cf579f552cd5f6c", size = 2049435, upload-time = "2026-10-11T18:33:10.366Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8e/f195aebec49ad12318876ac2368c197a7939f5d52f8e156d2238ef8a4588/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:992c3514ec891fa7858099183e4d64e6bd5a5d4ff452fae29df22faa77a006bb", size = 2172975, upload-time = "2026-10-11T18:33:12.368Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f5/7ad9fb83010cd5ea0948409db105676ed779c4e709e2d3d89b9fe2558794/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:739dc730e6be3bd5ec2f4ab5cfc7eb047cc45fc1497b3bafec74ff2ed07df597", size = 2364197, upload-time = "2026-10-11T18:33:14.244Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/bf0aab3e78301d202b82a0322ec968cd703b11fc0623fde9a28708936f62/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fad3a91e51b6d2039c572db04a5a873260b399f6bd62c3552671fa7a4a2899", size = 2444259, upload-time = "2026-10-11T18:33:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/38f6d6564fae57d9b12e5676dfa947e0e7d5c46dbeaa7eb3352261d6d299/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42b54c2c90ad348b5e3a85e03e715d572c1fde357ef104cdfe3b03b697a404ea", size = 2159857, upload-time = "2026-10-11T18:33:17.51Z" }, + { url = "https://files.pythonhosted.org/packages/65/a0/fb0a3ca10f139dcf765b2d312d10cf0f65c57c59993229d00ad12b14ceff/pydantic_core-2.50.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:2df1ff41884de2bc4b307bafd7c40a691094fad2ff8e767e5b45a319257bcf4e", size = 2233954, upload-time = "2026-10-11T18:33:19.591Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6a/e63842252702aa4ec6e6b7178ca85e541a77459076592c23ebe2d9840b33/pydantic_core-2.50.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe90228920fd8ff2be62622b6bb8a2b11acd65046d50c6b130614b5879605a20", size = 2243876, upload-time = "2026-10-11T18:33:21.393Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/5991cf8318b37e0dfab47b87541619a1df8501a793cba0d978846cba37a7/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:844b869f118e22a41a091bdcedda8a71bc1b0f62c38d1a0c3211cece47e1d8fc", size = 2277908, upload-time = "2026-10-11T18:33:23.324Z" }, + { url = "https://files.pythonhosted.org/packages/a7/3e/3ee8baaa6cc25a6961c69168cf9ff0f002d56f4e0d541ad6724c18fb61f3/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:2eb75304506894a281d346220a4f7481a1b8729577c5ed2a05395991966a8396", size = 2449285, upload-time = "2026-10-11T18:33:24.942Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/acbec6deac13fe697a80c275a9b6661db62c4d323bac8a47347b1f39c7cd/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6b20a4bffabdad0db2927ac034ae3b8a681b1f7a0182f3e60b479ad2fde21ebb", size = 2479013, upload-time = "2026-10-11T18:33:26.925Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/21a3f237b264389f6219d053ee78fc1b5c2fd402c1b1cb2b6cb8b85f9834/pydantic_core-2.50.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:99ba9bc2b8062ea0c326a990f7f00e6530c23579de66dd246e72c4cafef950a5", size = 1484400, upload-time = "2026-10-11T18:33:28.693Z" }, + { url = "https://files.pythonhosted.org/packages/09/ce/077a6d262d12ef09108377ac0717f029f420d773ace63cafd6143af75568/pydantic_core-2.50.1-cp314-cp314-win32.whl", hash = "sha256:cf356f70551d40374eaffb1aa63f1eb6d2006681cbd7a9faea173ce0f4dd7cd2", size = 2103222, upload-time = "2026-10-11T18:33:30.326Z" }, + { url = "https://files.pythonhosted.org/packages/14/4c/350a2415209c43d670eb71d3c040f332c04a30583d39e311b05c7ac15762/pydantic_core-2.50.1-cp314-cp314-win_amd64.whl", hash = "sha256:d32f3acc081cc3923386d88f422cde8892335e95f034e0104bb4cf9310d9915f", size = 2171586, upload-time = "2026-10-11T18:33:32.139Z" }, + { url = "https://files.pythonhosted.org/packages/bf/92/9bea6ca96580a0902fed366f064f0829e404f41889b34543279a1162b888/pydantic_core-2.50.1-cp314-cp314-win_arm64.whl", hash = "sha256:bed5163e03b98bc1fa2eb05d74c63d9c5c95d8ed6254985481640fbf5e237dea", size = 2155773, upload-time = "2026-10-11T18:33:33.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/8c/f121f073cf32bdd5cba7e6230b1ce0ac845b0cf43e44dd597d95af272db2/pydantic_core-2.50.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:9572c1369e9c9da2d64a7b7992c786d90ff295abc93964cfe3125e4290768070", size = 2234017, upload-time = "2026-10-11T18:33:35.588Z" }, + { url = "https://files.pythonhosted.org/packages/a4/69/2bb2bcd6146cffe98d760a46c42ae71efd5151d9b2f9c9bf6619a3b32083/pydantic_core-2.50.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2005207aafe1231315718bf6ed5d064a7300fb4772754af35ee72fc68159492e", size = 2001016, upload-time = "2026-10-11T18:33:37.57Z" }, + { url = "https://files.pythonhosted.org/packages/20/b3/fbf854c7d07ec114260c26e9e2071a4381740f9ae09641dbfcbdf2a18c45/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f6047f62a6c5ae08d0a6afb035667aa2d97c3d20d69762e034c5ea144d92a5", size = 2023015, upload-time = "2026-10-11T18:33:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/65/20/6de55b2f92cdb614b745c6e9ced639fc4fd7e1e77604825a88bdece6fcbd/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ef800dd7d85bcdadf4c3076e4c94e43939493558a3b69a1ea830c706d4617bb", size = 2192543, upload-time = "2026-10-11T18:33:41.381Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d9/19e91c94bd5c405945ce2f15526808aea37e162c253160de8ed7bf70b406/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0135bcdcaa0f23573f286e4cb5e0fd2962700964ed13df085b85f2b97aeab9e", size = 2388704, upload-time = "2026-10-11T18:33:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/b6/bc/2e24c8415eae1a25ee5a5484946a917123bad2e9d01689a759236928175a/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b3a6f334c6a2345ca15318ff894502a90012536404b37c844a976c76c846e0b", size = 2464118, upload-time = "2026-10-11T18:33:44.856Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1f/945b8053cb061c64e102bcaf7bfb9ed740c0bd4349f9bb978a7d4ddff4ab/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06e01fbbfdb9be777b316a71b6c49efaf4a08b615d0a98d678cda3023f79d019", size = 2150142, upload-time = "2026-10-11T18:33:46.661Z" }, + { url = "https://files.pythonhosted.org/packages/2b/78/96a3e50bf0d64aaae781107eb9335b7529d05a85793ed6e7241c4cd1d931/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a29a061fec0b4e2d714f277e70a3a18125ecff803f2fea6eade2f2e53711d112", size = 2245577, upload-time = "2026-10-11T18:33:48.574Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5d/a6038a0322232758a6ebfa709f4ca14a60bf5b93940cd0b345056a557da4/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f5187624823423e1d1b82b1072ac41dc837389e18d3d0572cc19bbee46cd550a", size = 2263728, upload-time = "2026-10-11T18:33:50.527Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4b/76ded3333a457a9344c4f2d63f2d82d0101654b05178fa4ddfe7d3627674/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3e46a9eb0a0901dd6275e6b06ac3a464885ef350ec4121fe486869de8053e4bb", size = 2293630, upload-time = "2026-10-11T18:33:52.362Z" }, + { url = "https://files.pythonhosted.org/packages/a9/00/9eca378335c9c1b72bc779bf6e4c2a82ce784f14ec48a0e87102c9870e05/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:756d669f04e62ec4148ecfe22be6a4484d9b1181a6ef32e205ebfd200540858b", size = 2468048, upload-time = "2026-10-11T18:33:54.118Z" }, + { url = "https://files.pythonhosted.org/packages/35/ca/e3832e9cf93651251de43c8c5c029ed680a3c1a0a1b17ee26c81bed08cbd/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:c516cc5367ca3448995d42cb994bf3f4c9002d2a7c22eac9622551269ad1b807", size = 2512159, upload-time = "2026-10-11T18:33:55.99Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f2/773469b5a10a39116a2cb17edaf6d722f017dd8da07161b371465311c542/pydantic_core-2.50.1-cp314-cp314t-win32.whl", hash = "sha256:9d1bed94af6a63835461f3cf7502058eb166c58c4778e11d0f433cfb1bd69e19", size = 2098406, upload-time = "2026-10-11T18:33:58.043Z" }, + { url = "https://files.pythonhosted.org/packages/ee/42/0bb74f8f25204b259b11ab7c12dc7f180893b6bd706118b385147fb6efd5/pydantic_core-2.50.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c8dce1f1e0e5358b682a6ad3fa5e31b31d4560997b8e61417e9217c8d60f8a0c", size = 2148481, upload-time = "2026-10-11T18:33:59.913Z" }, + { url = "https://files.pythonhosted.org/packages/47/0d/d801646c9679a4e630e15cf521d4108b93854b42a6e6e02391bd4c6b1095/pydantic_core-2.50.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ceff0acc940be2715bd6ad17b24c0e5304abf44f6efd0f81ee8499e640f9dc86", size = 2174972, upload-time = "2026-10-11T18:34:01.875Z" }, + { url = "https://files.pythonhosted.org/packages/b2/84/23984b763d8862a02a13d27a44b6e8169428fd85ecfed88f54c29108604d/pydantic_core-2.50.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:8a6791afa2245e6c6b180122d105941644f5bd410bb18623b408808cc41a3102", size = 2224599, upload-time = "2026-10-11T18:34:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/4d/90/a63cf8586abc1d1a3f6d9b18f0224789ebf003851f092ebda3c7c863fd32/pydantic_core-2.50.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:84f34323a61a365b4e9295de6028474754829aaddd59c7bf1a040e7487ef8f3c", size = 2024191, upload-time = "2026-10-11T18:34:05.901Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6a/f34bff9808ffb4907fbf5f5040457d253cacf2da7fce9efbc99ca1b1a44d/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23edad659e8dbd8ca7e4e877fe6c81573abbdf215bd25a68b53e1272f58b80c7", size = 2048252, upload-time = "2026-10-11T18:34:07.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/54/13f419bf1eb59852003818e25935aaf175687d978f4c4bca70e08fe40a3b/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a5fce22f1e87d181e924e12da7d81cfe031fb3881a5ddf26ad28f141756ca43", size = 2173376, upload-time = "2026-10-11T18:34:09.592Z" }, + { url = "https://files.pythonhosted.org/packages/6d/6c/b5a34d24cd0c81669d8f8339d74e6815abcf2f8fb48ab4b49b84c09be1d5/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c73622ef819328873b53109ee4f77ceb598bffedd02daf916102be3228866b78", size = 2364559, upload-time = "2026-10-11T18:34:11.534Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8d/d64d6216a8df365082665927ff923f183056f9049fee08e9777c9ac05296/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce8c25ca38cc0e3d7753ba180808de2c0c8cb24eae0df64491e40921454e9831", size = 2444697, upload-time = "2026-10-11T18:34:13.535Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0d/1b1149f60a00ea21ba5f70e28acbd40feb4598af414f80f53c921fac07c9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7689580e72a642ab5ec64d5f55b2e33636fa43b4ebe63c0c2c965ef307c7d1aa", size = 2159961, upload-time = "2026-10-11T18:34:15.56Z" }, + { url = "https://files.pythonhosted.org/packages/1e/35/f236549299dcc78e71e945495d7ec67e78d20844d0999c60601685003031/pydantic_core-2.50.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d5c0e32fdbce7f1e8ef4d11f655694bf5f4175c757a9f1dc2be09b8864e5bcf5", size = 2234581, upload-time = "2026-10-11T18:34:17.502Z" }, + { url = "https://files.pythonhosted.org/packages/6a/85/26901a490522b7f75ef9bb9a7afb73e5bb550f0e1cb5b99a0069183e2eb9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40f523349960fa30f3ea51404308ff50f9997a90df639590f47a057c1f32b415", size = 2244090, upload-time = "2026-10-11T18:34:19.402Z" }, + { url = "https://files.pythonhosted.org/packages/ca/2c/481bcfc70ceeb77a778ca6e5b705fe592cb64735c108157f81b7dd9c280e/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:d4193206b6587047437f6f11d7e776df23e1c1e23af2a54d9347275614791e10", size = 2277948, upload-time = "2026-10-11T18:34:21.317Z" }, + { url = "https://files.pythonhosted.org/packages/24/eb/f1e09333faa7ba447cde967310f758430cc7c5e987bdab5228816c70030d/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:84bc765b282a9d5b7fe0348b8648904f25a6a04b2139da52b1dd30c8ac3a2c8f", size = 2449688, upload-time = "2026-10-11T18:34:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b3/036bde636db8f76d92996e81aefc75678ab5cec4a07eea1ad0c72a893fc3/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:ed1e728b39a383c81035b2459cfcb35d99dfb01f7d6ebe3a913bc1cc5b81e459", size = 2479286, upload-time = "2026-10-11T18:34:25.214Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/07f018294ee18d144afeb6df1a47d9e92960f014be45c5ed13519fa5af95/pydantic_core-2.50.1-cp315-cp315-win32.whl", hash = "sha256:bc94f474417604bd383d2cd445d071b07dd55fedceed3ce33407bf1fcc107290", size = 2103683, upload-time = "2026-10-11T18:34:27.42Z" }, + { url = "https://files.pythonhosted.org/packages/5f/98/f9bd7e1f9b6709f155acb9ef826d9c3884fe925f811fd8e55b9b52280bad/pydantic_core-2.50.1-cp315-cp315-win_amd64.whl", hash = "sha256:983a662de2571cb2502fc8ff47b6770b03d025d2eb314c92f77b3f07c74720ed", size = 2171681, upload-time = "2026-10-11T18:34:29.508Z" }, + { url = "https://files.pythonhosted.org/packages/10/87/4bb3e1e7f385c076ab5af4d6dd0571b22042cd8207eb810d5b9fef15ae31/pydantic_core-2.50.1-cp315-cp315-win_arm64.whl", hash = "sha256:94845ff54dc5193f228cab81b2662a04bfbb892e95bdc15edf7399000ce57d54", size = 2156251, upload-time = "2026-10-11T18:34:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/83643225b08f2aef6c8cc4bbe6e3f79c5e139c4364a6e450d0f399d87774/pydantic_core-2.50.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:4a53d13cdfbedbfa87f08b83c1a0a5efcc767d785a4b41934fa9cb672670493a", size = 2234363, upload-time = "2026-10-11T18:34:33.65Z" }, + { url = "https://files.pythonhosted.org/packages/5e/66/127ca649ba2f2462039dc1e694c6c01e00a3689f023a617364d3507e6d97/pydantic_core-2.50.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:efbecf43d321f7b9281441f1f213f7c21c66988b0e06c2730ba13ed47a46bb08", size = 2000972, upload-time = "2026-10-11T18:34:36.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/e421e0a5d653b1203b090b2e48745976988d7338a647940704b5b9c2b399/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc1f08f68dac9f9e83845a8039880aba2ab553eb9b2259c3243a313182c253fe", size = 2022925, upload-time = "2026-10-11T18:34:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/ea5568e2491e1a71100f15ae8c2d01ef52db42a184a2d4e716bc79e5eb8f/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5dfe41f232befddb9c4377f6cfc702b51595e2d78ed082672adf8758d2c4619f", size = 2192867, upload-time = "2026-10-11T18:34:39.921Z" }, + { url = "https://files.pythonhosted.org/packages/ae/5e/3b8c3a35acbe219909ada5defad5d7d9fed845fb2b37bd5ec518f453c119/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adc06d218a1cadfd2ec4628424d7d79ce4eba69c2965e7e7b55106f0da5208c8", size = 2388687, upload-time = "2026-10-11T18:34:42.184Z" }, + { url = "https://files.pythonhosted.org/packages/b6/aa/7889b4e515f91a2e8c0ae6b5081fec0feb30c4398d1434e14793c60f173a/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cf91809d0721ab81592ba67bea7694821679c10b1a2e3c3460082b286c1918a", size = 2464415, upload-time = "2026-10-11T18:34:44.384Z" }, + { url = "https://files.pythonhosted.org/packages/08/78/93449e628eb8a6fdcce3eff9043081179d1bc7ce6f1bff32dc5006f41e00/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23923ab9292c40da026330b1ecf4dc2618c8e86e0422e5d1fbf50d94d64ca4f8", size = 2150823, upload-time = "2026-10-11T18:34:46.392Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f1/72c5bc129fceb0d00f05dc1e67f518c1728de1928c55f81fc13d7690de39/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:f3377c8c2b3ce898423c5e5dd94c7982e30aa7717a7e6ab2470b9de364963709", size = 2245806, upload-time = "2026-10-11T18:34:48.805Z" }, + { url = "https://files.pythonhosted.org/packages/28/2a/922a0e78f3aa6ab837b59f88190233fb8dad546c17995fde9bb3ed3b9b49/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:455a773617b5913bf5c20d0692e5787b119e52c4d40ea644ca31f5758fd31be2", size = 2264160, upload-time = "2026-10-11T18:34:50.876Z" }, + { url = "https://files.pythonhosted.org/packages/52/a8/0f1449e3e1b20941c9372faa18e7b6a092e30cdfa841902473f7989532ac/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:1a9006395dece0e32e704c315eff8a00bede494f6108546cfc5539c89fef4f9a", size = 2294031, upload-time = "2026-10-11T18:34:52.876Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d0/1031f492857de70355fb16524bbb03efce5fd34c93ed4a1ec60be07e0d4b/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:d2d82aa62521c55ddfb000ae70f88cdd8de974078f6024e821dfe5addd0c818f", size = 2468423, upload-time = "2026-10-11T18:34:54.995Z" }, + { url = "https://files.pythonhosted.org/packages/46/52/269ffffa645b8e47906395a39cf9db1151ae9fa4bcd7f47b960bc31baf37/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:009634b83993777ddcd69cad0ffcace43dabde692109528e35f0fde91e386a8b", size = 2512598, upload-time = "2026-10-11T18:34:57.149Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/e47e28281f20326ff6f3c31d626f0a83e615d2d94ba41bb0ad6ec237184a/pydantic_core-2.50.1-cp315-cp315t-win32.whl", hash = "sha256:3fde4fdc6487a58d944ca87cf5adc95d5f266e872c19599f5f4c0a8a1b1f9f9f", size = 2098535, upload-time = "2026-10-11T18:34:59.313Z" }, + { url = "https://files.pythonhosted.org/packages/03/ad/759e181e69c1b472c60b2049e5f61d5da1deefdd5ce1df4bb2ebcf771f25/pydantic_core-2.50.1-cp315-cp315t-win_amd64.whl", hash = "sha256:1c8632d4ac04e6f91128fca584b3a8a507d81604c24eeaaad00d4be42765c32b", size = 2148645, upload-time = "2026-10-11T18:35:01.571Z" }, + { url = "https://files.pythonhosted.org/packages/6d/56/8a702c27e5be9f47e5f19d8669227424290e4c024e7c370279cbaf244b4e/pydantic_core-2.50.1-cp315-cp315t-win_arm64.whl", hash = "sha256:c3ede305158e75510be50869b319550ab072008c13d64d4ab1e094fb286b6f44", size = 2175297, upload-time = "2026-10-11T18:35:04.079Z" }, + { url = "https://files.pythonhosted.org/packages/e7/73/a23a327237d9bb985298ce47702e196f995ce850224c12c70886bd66bb5c/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:062e891facce5ca296a1c37098e5e466780457f86413894b399f0cf22934f769", size = 2162560, upload-time = "2026-10-11T18:35:06.274Z" }, + { url = "https://files.pythonhosted.org/packages/df/b2/33b37b5e82408f402a5e82649a43e7c95cd5dab7beaeef2c35cedbfd18f2/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:49c2cbb2397fe4d0987e84606e691af6cb87bc0ee1bd3e7b737f7e10b4c142f9", size = 2075250, upload-time = "2026-10-11T18:35:08.455Z" }, + { url = "https://files.pythonhosted.org/packages/04/62/c7d5b9249466ccab90f3ce4790c2399dbfb8172ae9e74a7753dd295fe65a/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e4472072de0137ee0d8e72d6620e85939c271d2f90f6bbb4b15c24638b79f92", size = 2107891, upload-time = "2026-10-11T18:35:10.918Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7e/28d29bc1657195933147f772dee0558d9e0f045bbc11db7f992f40c37bb5/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ed4f3cef55164b026fefb41341b7754cc6b624c75dfe7142d2ecceb5ad21c87", size = 2268026, upload-time = "2026-10-11T18:35:13.336Z" }, + { url = "https://files.pythonhosted.org/packages/a5/09/1bcf160f3cd6333e2a76982fae5963e5032de0bcaaba38464223dc266bfe/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:76e2e83fa6ec8cdc972d438dafc2522b3a47bee4ec0ae668b29cfb1977ab5242", size = 2217070, upload-time = "2026-10-11T18:35:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/3d/81/f02de95eec7794dfc7a5bc69a834f0e834b31b3009014ce6de903855f775/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a51eee75939cf811ac09b278745a6cee7dc873ccfbc8b9af3cc88fe4b7ce25b5", size = 2039027, upload-time = "2026-10-11T18:35:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/4d/16/227746ed3771d9bf2304568b55134849ece34e13b38b1d76de763015c77f/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae28183297fb0d2b8dc46a1f01d51f5e45825fc5afe76a835a6cb7fb34821295", size = 2109009, upload-time = "2026-10-11T18:35:20.218Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b9/7664d1592a0e5a74903f357cd5b28ceb7336f3473256c5a4e8c8432f1881/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88e492e8b9d0312e7dc13667c30222abf284dc3b79b5302b3607b41a5784ce61", size = 2269883, upload-time = "2026-10-11T18:35:22.48Z" }, + { url = "https://files.pythonhosted.org/packages/bd/35/002a378302ff91d2a7f149bc8d22363faf581ba40ac39d0123444315a32d/pydantic_core-2.50.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7456d699b13954e9c0164dcb267250a10ae0dfb03e6e26d6796ab0d46e189c84", size = 2154076, upload-time = "2026-10-11T18:35:24.743Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5c/7a034330082c00f3c9795052bd9b161aaaf6e1951e897ac00a094823ef7e/pydantic_core-2.50.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0d955195bbbe489ad343fcc956eacea9357b79cb22192c66cacdefcbc14b32f", size = 2085850, upload-time = "2026-10-11T18:35:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/57/fa/d5b44a7d3d4eceed6ef8713a0003af56fefde6ec75915513bcdd5d730cd9/pydantic_core-2.50.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2c634642694e6a0dad2ab1d375589fa671fd442edd5caf7d9737b8f6ca22906", size = 2259754, upload-time = "2026-10-11T18:35:29.763Z" }, + { url = "https://files.pythonhosted.org/packages/b6/88/e01dd37301710bd8b3b2daf34ec9e2a6e83a333130831c0d468b0292a48c/pydantic_core-2.50.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6db2fbed51a7991302e8fac498cd67e336246026d0dfa84cf5166ce1412760", size = 2268712, upload-time = "2026-10-11T18:35:32.654Z" }, + { url = "https://files.pythonhosted.org/packages/3e/43/5bb80d4a4d2b2d84612a30205662cc521a20d209c94abc079ea48b4e0df2/pydantic_core-2.50.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:79490e33c4c0fcb933bbbcfc3a62184d8803b99f535863dfbb925e1bcb6945ad", size = 2305049, upload-time = "2026-10-11T18:35:35.392Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7b/e3ef76b269cfe4a5bd3bdcf5d7124cf05e5a8909d443b91639307116bdae/pydantic_core-2.50.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:48569b0ade9edfbe065cad1d700175546592aebbb42f02adcebcc26e75b896fe", size = 2466001, upload-time = "2026-10-11T18:35:37.791Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7a/ea0947aadc1d9f72d4be00d2e19939611ec17363bcb5e6eb9ec7d8221c34/pydantic_core-2.50.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5f3cae32fc46121f787cb2486de9cf95a8bf72aec5cc78f64c606fa1735a6ef5", size = 2481777, upload-time = "2026-10-11T18:35:40.287Z" }, + { url = "https://files.pythonhosted.org/packages/24/f3/d15bc0b1fb0c4f1e07b3326ffb7489f7ae70da2b56a0884d22641b0eb47c/pydantic_core-2.50.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7f476456ac2bb0d937f75191494a09c83a30765fea4f70f3b404942fe25f6cdf", size = 2336787, upload-time = "2026-10-11T18:35:42.558Z" }, ] [[package]] @@ -5762,7 +5762,7 @@ wheels = [ [[package]] name = "sqlspec" -version = "0.66.0" +version = "0.67.0" source = { editable = "." } dependencies = [ { name = "mypy-extensions" }, @@ -6398,11 +6398,11 @@ wheels = [ [[package]] name = "truststore" -version = "0.10.4" +version = "0.10.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/9f/c5201d42a484c061e528825fc8e2d565f5abd50a4ced6fb7d29c4ec99b2b/truststore-0.10.5.tar.gz", hash = "sha256:30d36967ccaded5cbb38d602c433f53600036c79d502f4533a49b60a03bbefcd", size = 28091, upload-time = "2026-10-12T22:27:31.808Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, + { url = "https://files.pythonhosted.org/packages/51/e9/3a7820be2bb0fe53b6bc9c3be26d3d1158004e4c3ab953aa6840b955b1e9/truststore-0.10.5-py3-none-any.whl", hash = "sha256:9aaaedaefaf06d8b206278cf8b5012bc897f485a874503501e12d776df78951c", size = 19017, upload-time = "2026-10-12T22:27:30.377Z" }, ] [[package]]