Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0f4ef8a
chore(release): bump to v0.67.0 and update changelog
cofin Oct 13, 2026
9e2c4fb
feat(bench): enforce metadata parity and add ratio comparison mode
cofin Oct 13, 2026
80e9c53
build(bench): derive adapter gate args from baselines and run all dri…
cofin Oct 13, 2026
71a280e
feat(bench): add slow-path, per-request, and postgres/oracle benchmar…
cofin Oct 13, 2026
01916fe
feat(bench): add interleaved stub-driver micro-benchmarks to subsyste…
cofin Oct 13, 2026
8df24ec
perf(core): share cached sqlglot ASTs with copy-on-access expression …
cofin Oct 13, 2026
d8ee695
fix(driver): skip statement cache when operation type is undetermined
cofin Oct 13, 2026
dbaeb7d
perf(driver): route parameterless and scalar positional calls through…
cofin Oct 13, 2026
95b20b9
perf(driver): share statement caches across sessions per database config
cofin Oct 13, 2026
09315a4
perf(driver): preserve fast-path statement execution when observabili…
cofin Oct 13, 2026
2fd5213
perf(driver): defer fast-path SELECT statement allocation and streaml…
cofin Oct 13, 2026
010267f
perf(driver): inline exception handler execution when no ambient exce…
cofin Oct 13, 2026
691742f
perf(core): precompute fast-path coercion fallbacks and style remap f…
cofin Oct 13, 2026
97a7d88
perf(driver): avoid redundant parameter scanning during execute_many …
cofin Oct 13, 2026
531caa3
fix(duckdb): unify row materialization under native Python type contract
cofin Oct 13, 2026
51a7910
perf(duckdb): cache bulk insert SQL fragments and streamline rowcount…
cofin Oct 13, 2026
631e41a
perf(aiosqlite): add idle connection acquire fast path and skip clean…
cofin Oct 13, 2026
0f7d804
chore(bench): update adapter performance baselines and changelog
cofin Oct 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 68 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/litestar-org/sqlspec/pull/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 <https://github.com/litestar-org/sqlspec/pull/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 <https://github.com/litestar-org/sqlspec/pull/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 <https://github.com/litestar-org/sqlspec/pull/834>`_)
* Added Python 3.15 support across package metadata and CI test workflows.
(`#835 <https://github.com/litestar-org/sqlspec/pull/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 <https://github.com/litestar-org/sqlspec/pull/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 <https://github.com/litestar-org/sqlspec/pull/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 <https://github.com/litestar-org/sqlspec/pull/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 <https://github.com/litestar-org/sqlspec/pull/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 <https://github.com/litestar-org/sqlspec/pull/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 <https://github.com/litestar-org/sqlspec/pull/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 <https://github.com/litestar-org/sqlspec/pull/838>`_)

v0.66.0 - Native AsyncIO Spanner adapter, extension stores, and sync/async parity
---------------------------------------------------------------------------------

Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,22 @@
"auto_pytabs.sphinx_ext",
"sphinx_copybutton",
"sphinx_click",
"click_extra.sphinx",
"sphinx_design",
"sphinx_paramlinks",
"sphinxcontrib.mermaid",
"numpydoc",
"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),
"click": ("https://click.palletsprojects.com/en/stable/", None),
"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),
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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}"
Expand Down
90 changes: 41 additions & 49 deletions sqlspec/adapters/aiosqlite/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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"}:
Expand Down
23 changes: 21 additions & 2 deletions sqlspec/adapters/aiosqlite/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion sqlspec/adapters/asyncpg/data_dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
__all__ = ("AsyncpgDataDictionary",)


_POSTGRES_METADATA_DOMAINS = (
_POSTGRES_METADATA_DOMAINS: tuple[str, ...] = (
"schemas",
"objects",
"tables",
Expand Down
2 changes: 1 addition & 1 deletion sqlspec/adapters/cockroach_asyncpg/data_dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
__all__ = ("CockroachAsyncpgDataDictionary",)


_COCKROACH_METADATA_DOMAINS = (
_COCKROACH_METADATA_DOMAINS: tuple[str, ...] = (
"schemas",
"objects",
"tables",
Expand Down
2 changes: 1 addition & 1 deletion sqlspec/adapters/cockroach_psycopg/data_dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
__all__ = ("CockroachPsycopgAsyncDataDictionary", "CockroachPsycopgSyncDataDictionary")


_COCKROACH_METADATA_DOMAINS = (
_COCKROACH_METADATA_DOMAINS: tuple[str, ...] = (
"schemas",
"objects",
"tables",
Expand Down
Loading
Loading