Skip to content

feat(spanner): event queue optimization, property graph AST, hybrid RRF search, and ADK artifact store - #834

Merged
cofin merged 12 commits into
mainfrom
feat/spanner-queues
Oct 9, 2026
Merged

cofin merged 12 commits into
mainfrom
feat/spanner-queues

Conversation

@cofin

@cofin cofin commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Summary

Optimizes Cloud Spanner durable event queues (sqlspec.extensions.events and sqlspec.adapters.spanner.events), extends the spanner and spangres SQLGlot dialects with Spanner Property Graph (CREATE/DROP PROPERTY GRAPH, GRAPH_TABLE) and Vector/Search Index AST support, and upgrades the Spanner ADK stores with batched UNNEST deduplication, 4-mode Hybrid Reciprocal Rank Fusion (RRF) + Vector + Full-Text Search, Property Graph DDL overlays, and SpannerSyncADKArtifactStore / SpannerAsyncADKArtifactStore.

Key Changes

1. Event Queue Runtime Hints & Transaction Lifecycle (sqlspec/extensions/events/, sqlspec/config.py)

  • EventRuntimeHints: Added cleanup_on_ack: bool = True and use_run_in_transaction: bool = False.
  • Read-Only Candidate Polling (_queue.py): Added _provide_session(read_only: bool = False) on SyncTableEventQueue and AsyncTableEventQueue so unlocked candidate reads (_fetch_candidate when select_for_update=False) request provide_session(transaction=False) when supported by the config. On Spanner, this uses a snapshot read instead of checking out a read-write transaction and issuing an empty Commit RPC on every poll.
  • Spanner Read-Write Transaction Wrapping (_queue.py): Routed write operations (_execute, publish_many) through driver.run_in_transaction(...) when use_run_in_transaction=True so Spanner read-write transactions automatically retry on transient Aborted conflicts.
  • Native TTL vs. Inline DELETE on ack(): Guarded the post-ack DELETE FROM {table} WHERE status = 'acked' AND acknowledged_at <= :cutoff query behind if self._hints.cleanup_on_ack: so backends with native row TTL (ROW DELETION POLICY on Spanner) skip unindexed full-table scan deletes on every acknowledgement.
  • Hash-Shard Configuration (EventsConfig, BaseEventQueueStore): Added shard_count: int = 1 to EventsConfig and _shard_count validation to BaseEventQueueStore.

2. Spanner Event Queue Store (sqlspec/adapters/spanner/events/store.py, sqlspec/adapters/spanner/config.py)

  • Session Transaction Default: Set _DEFAULT_SESSION_TRANSACTION: ClassVar[bool] = True on SpannerSyncConfig and SpannerAsyncConfig so provide_session(transaction=False) cleanly selects snapshot reads while preserving default read-write transactions when transaction=None.
  • Spanner Runtime Hints: Configured SpannerSyncConfig._get_event_runtime_hints and SpannerAsyncConfig._get_event_runtime_hints with cleanup_on_ack=False and use_run_in_transaction=True.
  • Hotspot Mitigation & Covering Index: Added optional shard_id hash-shard prefixing (MOD(FARM_FINGERPRINT(event_id), {shard_count}) in GoogleSQL; MOD(ABS(HASHTEXT(event_id)), {shard_count}) in PostgreSQL-dialect spangres) when shard_count > 1, and added STORING (payload_json, metadata_json, attempts, leased_until, created_at) (INCLUDE (...) in spangres) to the queue index to avoid base-table back-joins during dequeue polling.
  • PostgreSQL Dialect (spangres) DDL Support: Added full spangres DDL generation (VARCHAR, JSONB, BIGINT, TIMESTAMPTZ, TTL INTERVAL 'N days' ON acknowledged_at, and INCLUDE covering indexes) alongside GoogleSQL DDL (STRING, JSON, INT64, TIMESTAMP, ROW DELETION POLICY, STORING).
  • DDL Execution Override: Overrode create_table() and drop_table() on SpannerSyncEventQueueStore and SpannerAsyncEventQueueStore to route DDL through database.update_ddl(...) instead of DML execute_script().

3. Spanner & Spangres SQLGlot Dialects (sqlspec/dialects/spanner/)

  • Property Graph AST (_expressions.py, _parsers.py, _generators.py): Added SpannerPropertyGraph and SpannerGraphTable AST nodes, graph_table() builder helper, and parser/generator support for CREATE [OR REPLACE] PROPERTY GRAPH [IF NOT EXISTS] ... NODE TABLES (...) EDGE TABLES (...), DROP PROPERTY GRAPH [IF EXISTS], and GRAPH_TABLE (graph_name MATCH ... COLUMNS (...)).
  • Vector & Search Index DDL: Added IF NOT EXISTS and STORING (...) support on CREATE VECTOR INDEX, added DROP VECTOR INDEX [IF EXISTS] and DROP SEARCH INDEX [IF EXISTS] via SpannerDropIndex, and added HIDDEN column constraint parsing/generation for TOKENLIST AS (TOKENIZE_FULLTEXT(...)) HIDDEN generated columns (including compatibility with compiled sqlglot[c]).
  • Search & Vector Expressions: Extended approx_cosine_distance(column, vector, options=...) to emit options => JSON '...' via exp.Kwarg, and added snippet() and score_ngrams() expression builders.
  • Transform Preservation: Replaced SPANNER_TRANSFORMS.update(...) and SPANGRES_TRANSFORMS.update(...) in _generators.py with loop-based registration so module re-imports never overwrite custom Spanner expression transforms with base SQLGlot transforms.

4. Spanner ADK Memory & Artifact Stores (sqlspec/adapters/spanner/adk/, docs/)

  • Batched UNNEST Deduplication (store.py): Replaced the N+1 per-entry _event_exists snapshot read loop in SpannerSyncADKMemoryStore._insert_memory_entries with in-memory batch deduplication plus a single SELECT event_id FROM {table} WHERE event_id IN UNNEST(@event_ids) query and transaction.batch_update(...) inside one run_in_transaction call, backed by CREATE INDEX idx_{table}_event_id ON {table}(event_id).
  • Vector Embeddings & 4-Mode Hybrid RRF Search (store.py): Added optional embedding ARRAY<FLOAT32>(vector_length=>N) column and CREATE VECTOR INDEX DDL (vector_index_enabled, vector_dimensions, vector_distance_type, vector_tree_depth, vector_num_leaves), upgraded FTS mode to rank by SCORE(content_tokens, @query) DESC, and implemented 4-mode search_entries:
    1. Hybrid RRF (enable_hybrid_search=True + use_fts=True + embedding provided): Combines APPROX_COSINE_DISTANCE vector similarity and SEARCH/SCORE lexical search via Reciprocal Rank Fusion (1.0 / (@rrf_k + rank)).
    2. Vector Similarity (embedding provided): Orders by COSINE_DISTANCE / APPROX_COSINE_DISTANCE.
    3. Full-Text Search (use_fts=True): Filters with SEARCH(content_tokens, @query) and orders by SCORE(content_tokens, @query) DESC.
    4. Simple Substring Search: Case-insensitive LOWER(content_text) LIKE fallback.
  • Property Graph Overlay (store.py): Added optional CREATE PROPERTY GRAPH / DROP PROPERTY GRAPH DDL generation (enable_memory_graph, memory_graph_name, memory_nodes_table, memory_edges_table) and ordered _spanner_drop_statement_table so dependent property graphs and indexes are dropped before base tables.
  • ADK Artifact Stores (artifact_store.py): Added SpannerSyncADKArtifactStore and SpannerAsyncADKArtifactStore implementing BaseSyncADKArtifactStore and BaseAsyncADKArtifactStore with composite primary key (app_name, user_id, session_id, filename, version), session_id = "" normalization for user-scoped artifacts, and optional ROW DELETION POLICY TTL cleanup.
  • Documentation: Updated docs/usage/adapters/spanner.rst and docs/usage/driver_Patterns/events.rst with Spanner event queue sharding, Property Graph AST, Hybrid RRF search, and ADK Artifact Store usage.

Verification

  • Unit Tests: 891 passed across tests/unit/extensions/test_events/, tests/unit/adapters/test_spanner/, tests/unit/dialects/test_spanner*.py, and tests/unit/dialects/test_spangres*.py.
  • Static Analysis & Type Checking: ruff check, mypy, pyright, and slotscheck pass with 0 errors.
  • Documentation Build: sphinx-build -M html docs docs/_build/ -W --keep-going passes with 0 warnings.

@codecov-commenter

codecov-commenter commented Oct 8, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.91615% with 98 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.76%. Comparing base (3cedc7d) to head (b45c3f3).

Files with missing lines Patch % Lines
sqlspec/adapters/spanner/adk/artifact_store.py 80.85% 41 Missing and 8 partials ⚠️
sqlspec/adapters/spanner/adk/store.py 88.31% 9 Missing and 9 partials ⚠️
sqlspec/dialects/spanner/_parsers.py 87.91% 7 Missing and 11 partials ⚠️
sqlspec/extensions/events/_queue.py 83.67% 4 Missing and 4 partials ⚠️
sqlspec/dialects/spanner/_generators.py 94.94% 1 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #834      +/-   ##
==========================================
+ Coverage   81.72%   81.76%   +0.03%     
==========================================
  Files         521      522       +1     
  Lines       79815    80462     +647     
  Branches    11534    11628      +94     
==========================================
+ Hits        65228    65786     +558     
- Misses      10862    10919      +57     
- Partials     3725     3757      +32     
Flag Coverage Δ
integration 62.44% <43.15%> (-0.21%) ⬇️
py3.10 80.10% <87.79%> (+0.05%) ⬆️
py3.11 80.10% <87.79%> (+0.04%) ⬆️
py3.12 80.10% <87.79%> (+0.05%) ⬆️
py3.13 80.10% <87.79%> (+0.04%) ⬆️
py3.14 81.12% <87.64%> (+0.05%) ⬆️
unit 72.71% <87.54%> (+0.14%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
sqlspec/adapters/spanner/adk/__init__.py 100.00% <100.00%> (ø)
sqlspec/adapters/spanner/config.py 91.78% <100.00%> (+0.03%) ⬆️
sqlspec/adapters/spanner/events/store.py 99.20% <100.00%> (+0.48%) ⬆️
sqlspec/config.py 94.12% <100.00%> (+0.01%) ⬆️
sqlspec/dialects/spanner/__init__.py 100.00% <100.00%> (ø)
sqlspec/dialects/spanner/_expressions.py 100.00% <100.00%> (ø)
sqlspec/extensions/events/_hints.py 93.75% <100.00%> (+0.41%) ⬆️
sqlspec/extensions/events/_store.py 95.83% <ø> (ø)
sqlspec/dialects/spanner/_generators.py 74.68% <94.94%> (-0.49%) ⬇️
sqlspec/extensions/events/_queue.py 85.49% <83.67%> (-0.62%) ⬇️
... and 3 more

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cofin
cofin merged commit 9b2ba05 into main Oct 9, 2026
24 checks passed
@cofin
cofin deleted the feat/spanner-queues branch October 9, 2026 02:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants