Alpha Version: You are viewing the ALPHA documentation. This is an experimental version and may contain breaking changes.
Skip to main content

QueryDb — Local

The local QueryDb is the dev/test counterpart of the QueryDb → DynamoDB adapter: read-model storage for projections. It ships two backends that implement the same QueryDb_Adapter interface:

BackendSourceSelected by
In-memoryQueryDb/QueryDbStorage_InMemory.resBackend.Memory (default)
SQLiteQueryDb/QueryDbStorage_Sqlite.resBackend.Sqlite / REVENTLESS_LOCAL_BACKEND=sqlite

Both expose identical load / save / delete (+ batch and stream) operations and publish the same state-change descriptors on the bus. The notable difference is fidelity to the AWS feature set: the in-memory backend ignores secondary indexes and TTL, while the SQLite backend honours both (see below).

Operations

OperationDescription
loadReturns items for a given partition ID
loadStreamStream variant of load
saveStores a single item by ID (and optional sub-key)
saveBatchStores multiple items
countReturns the increment value (no actual counter tracking)
deleteRemoves an item by ID (and optional sub-key)
deleteBatchRemoves multiple items

Bus Integration

Each backend's Make(Bus, …) functor registers three functions on the bus:

  • registerQueryDb — the full operations record for GraphQL resolvers
  • registerQueryDbScan — a () => array<JSON.t> function for full scans
  • registerQueryDbStream — a () => Stream<JSON.t> function for streaming scans (used by QueryEngine with ~limit)

save/delete also publish Updated/Removed state-change descriptors on the bus so subscriptions and live-update paths fire locally.

In-Memory Backend

Source: QueryDbStorage_InMemory.res

Data Structure

Items are stored in a Dict<string, array<JSON.t>> (a ref), keyed by partition ID. A secondary allItems array is kept in sync for scan operations.

Limitations

Secondary indexes are ignored — every lookup is by primary key — and TTL is ignored (items never expire). This is sufficient for most tests; switch to SQLite when a test depends on index-shaped queries or TTL expiry.

SQLite Backend

Source: QueryDbStorage_Sqlite.res

Schema

One table per registered QueryDb, named qdb_<name> (dashes become underscores):

CREATE TABLE qdb_<name> (
partition_key TEXT NOT NULL,
sub_key TEXT NOT NULL DEFAULT '',
item TEXT NOT NULL, -- JSON.stringify of the item
expires_at INTEGER, -- unix epoch seconds; NULL = never expires
PRIMARY KEY (partition_key, sub_key)
);

sub_key is derived from the configured subIdField (empty string for single-key tables). save is an upsert (ON CONFLICT(partition_key, sub_key) DO UPDATE).

Secondary Indexes

Each declared indexConfig creates a SQLite index over a json_extract(item, '$.<field>') expression, so GSI-shaped lookups are actually indexed. Composite keys (pkFields / skFields joined by their separators) become a single concatenated expression. The DynamoDB projectionType is recorded for parity but is irrelevant in SQLite — every column lives on the same row, so there is no covering-index distinction.

TTL

TTL is honoured via lazy expiry: every read clause carries (expires_at IS NULL OR expires_at > strftime('%s','now')). There is no background sweeper — expired rows linger on disk until they are next read (filtered out) or overwritten by a new save.

Key Differences from AWS

AspectLocal (in-memory)Local (SQLite)AWS (DynamoDB)
StorageDict + allItems arrayOne qdb_<name> table per QueryDbOne table per QueryDb
Secondary indexesIgnoredjson_extract indexes (incl. composite)GSIs with configurable projections
TTLIgnoredLazy expiry via expires_atDynamoDB TTL attribute
PersistenceNone (ephemeral)Durable (across restarts)Durable
AppSyncNoneNoneDataSource integration for GraphQL