All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- The wasm engine gained an operation-level
executeAPI, and a new npm package,@nubo-db/dynoxide-engine, that ships it. The Worker answers a small versioned RPC -open,execute,capabilities,contractVersion- with{id, op, payload}in and{id, ok, result|error}out, and a bundledEngineClientowns the round trip so you deal in objects instead of hand-building postMessage envelopes.npm run build:wasmassembles the package: the Worker, the two.wasm, theEngineClient, and amanifest.jsonstamped with the engine and contract versions. Depend on that built package, not this repo's source. The client checks itsCONTRACT_VERSIONagainst the engine on boot and fails loudly if they differ, so a stale embed can't quietly mis-read a newer one. Still a preview: the wasm path isn't run against the conformance suite.
- A
StorageBackendtrait in the newdynoxide::storage_backendmodule, decoupling the data layer from a specific SQLite binding. The native rusqlite-backedStorageimplements the trait, and the action handlers andDatabasenow consume it (see Changed). The trait surface also carries aclock()accessor for the stream and TTL paths and batch-shapedput_base_items/insert_gsi_itemsmethods that replaced the last two rawStorage::conn()escape hatches in the handlers. - A
BackendErrorenum returned by the trait surface, with an explicitrusqlite::Error -> BackendErrormapping for the common failure modes (NotADatabase, locked / busy, constraint violations, I/O failures), plus anUnsupported { capability }variant for a capability a backend cannot serve (the wasm preview uses it for TTL). It is#[non_exhaustive]so future backends can add failure modes without a breaking change. - A
Clockcapability onStorageso the trait surface does not assumestd::time. Stream and TTL paths route theircreated_atand sweep timestamps through the clock;SystemClockis the default andManualClockships as a deterministic test helper. Otherstd::timecall sites (idempotency cache, action-handler timestamps, snapshots) remain native-only and are unchanged. - A
wasm-sqlitecargo feature and a working WebAssembly backend. dynoxide compiles towasm32-unknown-unknownand runs in the browser against wa-sqlite (a WASM build of SQLite) over a wasm-bindgen bridge, persisting to OPFS.WasmBridgeBackendimplementsStorageBackend, andWasmDatabase(Database<WasmBridgeBackend>) exposes the handlers asasync fnwith noblock_on. It covers create-table, put, get, delete, query, and scan over base tables and both index types (GSI and LSI), with index fan-out atomic with the base write. TTL returnsBackendError::Unsupported; streams return a preview "not yet implemented" error pending a delivery design;TransactWriteItems, tags, table-setting updates, stats, and bulk import are preview placeholders. The native and wasm backends share one set of SQL builders (storage_backend::sql_builders), so both issue identical SQL. - A self-contained browser build:
npm run build:wasm(wasm-pack + esbuild) emits adist/of three files - a bundled Web Worker plus the two.wasmassets (dynoxide ~550 KB, wa-sqlite ~545 KB; ~1.2 MB total). The engine runs in a Web Worker because wa-sqlite's OPFS persistence uses synchronous access handles, which browsers expose only in a Worker; pairing wa-sqlite's synchronous VFS (AccessHandlePoolVFS) with its non-async build needs noSharedArrayBuffer, and so no cross-origin isolation (COOP/COEP) - it drops onto ordinary static hosting. A build-visibleWASM_PREVIEWconstant (trueunderwasm-sqlite) marks the preview. The harness underharness/loads the same bundled Worker that ships, so a green harness means the shipping artefact works; it exercises CRUD, GSI query/scan, and error-envelope fidelity on OPFS. CI builds thewasm32-unknown-unknowntarget for both thewasm-sqliteandwasm-harnessfeatures on every PR, so the harness's use ofWasmDatabaseand the action types is type-checked too. - Official Docker image.
docker run -p 8000:8000 ghcr.io/nubo-db/dynoxideis a ~5 MB drop-in foramazon/dynamodb-localin containerised test suites: multi-arch (linux/amd64andlinux/arm64),FROM scratch, published to GHCR on each release with Docker Hub and ECR Public mirrors pushed best-effort. The image ships aHEALTHCHECKbacked by a newdynoxide healthchecksubcommand, sodocker psand Compose health gates report status without extra tooling (#3). SECURITY.md, documenting the MCP HTTP transport's threat model: the bearer-token authentication it now requires, plus the Host and Origin allowlists that back it (#27).- MCP HTTP transport options:
--mcp-host/--hostto bind beyond loopback,--mcp-allowed-host/--allowed-hostto accept additionalHostheaders by name, and--mcp-no-auth/--no-authto disable authentication on loopback binds only. With a token set, these make the transport reachable from outside a container, unblocking the Docker MCP path (#24).
Databaseis now generic over its storage backend:Database<S>, monomorphised, nodyn. The parameter defaults to the native rusqlite backend, so existing code that namesDatabaseis unaffected, and a newNativeDatabasealias names that default explicitly. The action handlers are nowasyncand route through theStorageBackendtrait.NativeDatabasekeeps the historical synchronous public API: each method drives the handler future to completion withblock_on(viapollster), and because the native backend's futures never suspend, thatblock_onnever parks the thread, so it stays safe inside the tokio-based HTTP and MCP servers.DynoxideErroris now#[non_exhaustive]. Match arms in downstream code must include a wildcard. Done now, while 0.10.0 is already a breaking release, so later variant additions stay non-breaking.- Breaking: the MCP HTTP transport (
dynoxide mcp --http,dynoxide serve --mcp) now requires bearer-token authentication on every request. On a loopback bind, dynoxide generates a token on first run, persists it to a per-user config file, and prints a client-config snippet; later runs reuse it silently. Existing clients break until updated: add"headers": { "Authorization": "Bearer <token>" }to your MCP client config. A non-loopback bind requires an explicit token via--mcp-token/--tokenorDYNOXIDE_MCP_AUTH_TOKENand will not start without one. The stdio transport is unaffected (#27). - Breaking (library API):
dynoxide::mcp::serve_httpandserve_http_with_shutdownnow take anHttpOptionsstruct (bind host,AuthMode, extra allowed hosts) in place of a bareport: u16. Embedders constructing the MCP HTTP server must buildHttpOptionsand choose anAuthMode. rusqliteis now an optional dependency behind thenative-sqlitefeature (on by default, so native builds are unchanged). The crate type-checks with rusqlite absent, which is the precondition for the wasm build. Cross-platform wall-clock paths (the idempotency cache,created_atstamps, andSystemClock) moved toweb-time-std::timeon native, the browser clock on wasm. The native binary now builds behind aclimarker feature (pulled in byhttp-server,mcp-server, andimport), so it is skipped in backend-neutral builds such as--features wasm-sqlite. TheDynoxideError::SqliteErrorvariant is consequentlynative-sqlite-gated and absent on backend-neutral builds, which matters only for code that matches it by name on a wasm target.
- PartiQL
DELETEandUPDATEnow evaluate the non-key predicates in aWHEREclause instead of acting on the key alone. Before, the executor pulled the primary key out of theWHEREand ignored the rest, soDELETE FROM "t" WHERE pk = 'a' AND NOT begins_with(name, 'x')deleted the row even whennamebegan withx, mutating a row the filter should have excluded (a data-correctness bug predating v0.9.5). The write paths now run the full condition against the fetched item, the samematches_wherepassSELECTalready uses: a present item whose non-key predicate is false raisesConditionalCheckFailedException, matching how AWS treats a PartiQL write whose condition fails, and a missing item stays a silent no-op (#54). DescribeTablenow returns a stableTableIdinstead of a freshly generated UUID on every call. The id is a random UUID assigned once at create time and persisted (a newtable_idcolumn, added to existing databases through the versioned schema migration and backfilled), so it stays the same across calls,CreateTablereturns the same value, and a dropped-and-recreated table gets a new one, matching AWS (#55).UpdateItemevaluates anUpdateExpressionagainst the pre-update item image and accepts parenthesised arithmetic.SET a = :v, b = anow givesbthe old value ofarather than the value assigned earlier in the same call, andSET c = (c - :v)parses and applies on theBigDecimalpath instead of being rejected withExpected operand in SET, got ((#35).UpdateItemReturnValues: UPDATED_NEWmatches AWS granularity. A nestedSET parent.child = :vreturns only the changed fragment{parent: {M: {child}}}instead of the wholeparentmap, and a REMOVE-only update omitsAttributesentirely rather than returning an empty map (#36).- Paginating a
Queryover a GSI no longer drops items when several entries share the same index key and the base table has only a partition key. On a hash-only base table the continuation cursor lost its base-key component and stalled after the first page, the same defect #38 fixed forScan; theQuerypath now carries the base partition key, so every tied item is returned across the paged walk (#52). TransactWriteItems,TransactGetItemsand PartiQLExecuteStatementnow reportConsumedCapacitythe way AWS does. A transactional write charges 2 WCU per item and a transactional read 2 RCU per item including a missing one (each item rounded up before the 2x factor); theTransactGetItemsINDEXESbreakdown carriesTable.ReadCapacityUnits; andExecuteStatementreturns theConsumedCapacityblock wheneverReturnConsumedCapacityis requested instead of omitting it (#37).- PartiQL
ExecuteStatementaccepts the bracketIN [...]list form, not justIN (...), and evaluatesNOT begins_with(...)as a negated predicate.IS NOT MISSINGalready evaluated; the gaps were the bracket list and theNOTfunction arm, which the same statement bundles together (#40). DescribeTablenow round-tripsOnDemandThroughputand reports the fullSSEDescriptionshape. A table created withOnDemandThroughputreports itsMaxReadRequestUnitsandMaxWriteRequestUnitsback; the value lives in a newon_demand_throughputcolumn added through the versioned schema migration, so existing on-disk databases pick it up on open. Server-side encryption enabled with the AWS-managed key now reportsSSEType: KMSand aKMSMasterKeyArnalongsideStatus: ENABLED, where before it returned the status alone (#44).UpdateTablenow accepts a loneTableClassorOnDemandThroughputchange instead of rejecting it withAt least one of ProvisionedThroughput, BillingMode, ... is required. Both fields are validated (an unknownTableClassis aValidationException) and persisted, so the change shows up on the nextDescribeTable(#45).DeleteTableon a table with deletion protection enabled now returns the exact AWS message,Resource cannot be deleted as it is currently protected against deletion. Disable deletion protection first., in place of the ARN-prefixed wording dynoxide used before (#46).TransactGetItemsnow omitsItemfrom a response entry when aProjectionExpressionmatches no attribute on an otherwise-present item, matching AWS. The projection always re-injects the table key, so the entry previously came back as a key-only object instead of being omitted (#39).BatchWriteItemnow rejects aPutRequestwhose item is missing the table key with a 400ValidationExceptionrather than a 500InternalServerError. The duplicate-key detection pass extracted keys before validating them; it now validates first, the same ordering the single-item write paths already use (#39).- Paginating a
Scanover a GSI no longer drops items when several entries share the same index key and the base table has only a partition key. On a hash-only base table the continuation cursor lost its base-key component and stalled after the first page; it now carries the base partition key, so every tied item is returned across the paged walk (#38). - A single-item write (
PutItem,DeleteItem,UpdateItem) and its GSI/LSI index fan-out now run in a single transaction. A failure partway through the fan-out rolls the whole write back rather than leaving a base row with a half-applied (torn) index. The same per-item atomicity now also coversBatchWriteItem(each write request) and the TTL sweep (each expired-item delete). This matches DynamoDB, where a single-item write does not half-apply to its indexes. - Write paths now roll back on a failed
COMMITand surface a failedROLLBACKrather than leaving the connection stuck mid-transaction, which would make the next write fail. Every write path shares one transaction helper for this. - A client-facing
ValidationExceptionraised inside a backend method (the 50-tag limit inset_tags) keeps its 400 status across theStorageBackendboundary instead of collapsing to a 500. - Tighter expression and scan validation, to match what real DynamoDB rejects
(surfaced by the conformance suite). Dynoxide now turns away redundant
parentheses like
((a = :b))in condition, filter, and key-condition expressions;contains(x, x)with the same operand on both sides; andbegins_withhanded a number instead of a string or binary. These are rejected up front, before any items are scanned (#31). size()now measures strings in UTF-16 code units rather than bytes, so values with emoji or accented characters report the length DynamoDB returns.- A negative
Segmenton a parallel scan is now rejected rather than accepted.
- Existing native code that names
Databasekeeps working unchanged: the new generic parameter defaults to the rusqlite backend and the synchronous method signatures are identical. The one deliberate behaviour change is index fan-out atomicity (see Fixed); it is more DynamoDB-correct, and the conformance suite still passes. Tests, conformance, and benchmarks pass against the same observable surface as before. - Building dynoxide for
wasm32-unknown-unknownis now supported via thewasm-sqlitefeature (see Added). The wasm backend is a preview: it is not run against the conformance suite that covers the native build, so its correctness rests on its own CRUD/query/scan/GSI/LSI tests for now. The engine runs in a Web Worker (OPFS's synchronous file handles are Worker-only) and needs no cross-origin isolation, so it works on ordinary static hosting.
-
Close a DNS rebinding vulnerability in the MCP HTTP transport (GHSA-89vp-x53w-74fx / CVE-2026-42559) by upgrading
rmcpfrom 1.1.1 to 1.6.0 in both lockfiles. A malicious page could make the user's browser send requests to a loopback MCP server with a non-loopbackHostheader, which the server would then process. Affects 0.9.3 to 0.9.12. Users runningdynoxide mcp --httpordynoxide serve --mcpshould upgrade; stdio transport is unaffected. -
Close a related cross-origin CSRF gap: a page could
fetchthe loopback endpoint withmode: 'no-cors', and the Host check would pass while the Origin header went unchecked. Affected write tools:put_item,update_item,delete_item,create_table, andbatch_write_item. Fixed by setting an explicit Host and Origin allowlist onStreamableHttpServerConfig. Native MCP clients (Claude Code, Cursor, the dynoxide CLI) don't send an Origin header and are unaffected.
-
Unix: port releases immediately after
dynoxide serveshuts down. The listener used to skipSO_REUSEADDR, leaving leftoverTIME_WAITsockets from connected clients to block restart for ~60s. Live-listener conflict detection is unaffected:SO_REUSEADDRonly bypassesTIME_WAIT, not active sockets.Windows: unchanged.
SO_REUSEADDRlets another process hijack an active bind there, so we leave it off.
dynoxide serve --mcpnow exits cleanly on Ctrl+C when an MCP client (Claude Code, Cursor) is holding a connection open. The MCP server's graceful-shutdown drain used to wait for those connections forever, hanging the process until something SIGKILLed it (#22)
- Refresh
Cargo.lockfor the dependabot patches reachable within MSRV:aws-lc-sys0.37.1 to 0.40.0 (5 high-severity AWS-LC issues),openssl0.10.75 to 0.10.79 (5 buffer-overflow advisories),rand0.8.5 to 0.8.6. Remainingrustls-webpki/time/aws-sdk-dynamodbalerts are dev-dependency only (test-suite AWS SDK chain, not the production binary) and stay pinned by MSRV 1.85 until v0.10.0
- 16 places where dynoxide's error strings drifted from real AWS DynamoDB. Mostly small things you only notice when you assert the message:
tableNamelength validation is now per-operation (1 char on read/write, 3 stays onCreateTable),Selectenum order matches AWS rather than alphabetical,QueryvsScanLimit=0messages are different on purpose now, batch/transact empty and oversize requests use the standard validation envelope, andUpdateExpression/ProjectionExpressionsyntax errors include the AWSnear: "..."window (#11, #12, #13, #15, #16, #17, #18) TransactGetItemswith a bad action key now comes back as aTransactionCanceledExceptionwithValidationErrorrather than HTTP 500. The 500 was a real leak: the dedup loop called the server-fault helper before key validation (#19)
KeyConditionExpressionnow accepts parenthesised sub-expressions, matching DynamoDB. Forms like(#pk = :pk) AND (#sk = :sk)previously returnedValidationException: Expected attribute name, got (. Both outer-wrap and per-condition parens are now handled (#4, #7)UpdateItemandTransactWriteItems.Updatenow evaluateConditionExpressionagainst the existing item before populating key attributes for upsert. Previouslyattribute_exists(pk)on a non-existent key succeeded and created a ghost item (#5)- Paginated
Scanon a GSI now returns all items when multiple items share the same GSI partition key. Previously the second page returned 0 items because the pagination cursor used only(gsi_pk, gsi_sk)instead of the full 4-tuple primary key (#6) <>on missing attributes now returns true, matching DynamoDB. All other comparison operators continue to return false on missing operands. Previously<>also returned false, breakingPutItemconditional idioms likestatus <> "working"against fresh keys (#8)
- Dynoxide no longer orphans when backgrounded in npm scripts (
dynoxide & sleep 1 && npm run seed && react-router dev) -- the port is released when the parent process exits (nubo-db/dynoxide#2) - The Rust server now handles SIGTERM for graceful shutdown, not just SIGINT (Ctrl+C) --
kill <pid>now works as expected - The npm wrapper switches from
spawnSyncto asyncspawnwith explicit signal forwarding (SIGINT, SIGTERM, SIGHUP) and double-signal SIGKILL escalation - Parent-death detection via PPID polling catches the backgrounded case where no signal is delivered to the wrapper
- Benchmark sanity checks were blocking README updates during release - 10 stale values from the v0.9.6 pipeline now corrected
- Binary download size in README was wrong (~5 MB, actually ~3 MB compressed / ~6 MB on disk)
- Docker image sizes now show both download and on-disk measurements - the old "225 MB" was the compressed download, the actual on-disk size is 471 MB
- MCP tool count in README was 33, should be 34 -
execute_transaction_partiqlwas missing from the list - npm README had incorrect
--inputand--db-pathflags for the import command (should be--source,--schema,--output) - Dropped the
servesubcommand from npm examples (baredynoxide --port 8000is the preferred form)
- Restructured release pipeline for token efficiency and reliability - dispatch verification, idempotent crate/npm publishing, template-based Homebrew formula updates
- npm publishing uses OIDC provenance via a dedicated
npm.ymlworkflow - Cross-compilation switched to cargo-zigbuild for aarch64-musl targets
- Commit Cargo.lock for reproducible CI builds (was previously gitignored)
- Updated npm package README to reflect current CLI usage and features
- Updated
aws-lc-sys0.37.1 to 0.39.1 (10 high-severity advisories - PKCS7 verification bypass, timing side-channel in AES-CCM, CRL/name constraint issues) - Updated
rustls-webpki0.103.9 to 0.103.10 (2 medium-severity CRL Distribution Point matching issues)
- Statically link the MSVC C runtime on Windows so the release binary no longer requires VCRUNTIME140.dll
- Switch Linux aarch64 target to musl for fully static binaries (matching x86_64)
- Drop the separate x86_64-unknown-linux-gnu release target (the musl build is already fully portable)
- DynamoDB conformance suite — 526 independently written tests across 3 tiers, validated against real DynamoDB ground truth. Dynoxide: 100%. DynamoDB Local: 92%. See dynamodb-conformance.
- Dynalite external conformance — 817/1039 passing (87.1% DynamoDB parity) against Dynalite's test suite, where real DynamoDB itself only passes 51%
- DynamoDB compatibility audit — code-verified compatibility matrix with file/line references, public-facing summary with DynamoDB Local comparison column, prioritised gap tracking
- Correctness audit — 41 issues identified and resolved across core operations and PartiQL
- Reserved word validation — 573 DynamoDB reserved keywords rejected in ConditionExpression, UpdateExpression, FilterExpression, and ProjectionExpression with correct error messages
- README benchmark automation — CI benchmark numbers auto-updated via template markers and Python script; PR-based review with sanity checking
- IdempotentParameterMismatchException — TransactWriteItems detects same token with different payload
- AccessDeniedException — returned for tag operations on non-existent ARNs (matches DynamoDB behaviour)
- BigDecimal replaces f64 for all number comparisons and arithmetic — eliminates silent precision loss beyond 15 significant digits; f64 fast-path for ≤15 significant digits preserves performance
- PartiQL INSERT now fails with
DuplicateItemExceptionif item already exists (previously silently overwrote) - PartiQL tokeniser — correct handling of negative numbers, escaped single quotes, unknown characters (error instead of silent skip)
- Query/Scan COUNT now returns filtered count, not scanned count, when
FilterExpressionis present - begins_with sort key — SQL LIKE wildcards (
%,_) properly escaped - Condition + write operations wrapped in SQLite transactions to prevent TOCTOU races
- 1MB response limit now counts all scanned items, not just filtered results
- GSI query/scan LastEvaluatedKey now includes base table key attributes
- BatchWriteItem rejects duplicate keys within the same request
- TransactWriteItems — 4MB size check uses accurate item size calculation; CancellationReasons returned as structured top-level JSON field;
ReturnValuesOnConditionCheckFailurereturns ALL_OLD item on condition failure - UpdateItem rejects empty update expressions; protects key attributes from REMOVE/ADD/DELETE
- ReturnValues validated against allowed values per operation
- UnprocessedKeys in BatchGetItem preserves per-table settings
- SET on list index beyond bounds extends the list with NULL padding (previously returned error)
- SET on empty list at index 0 now succeeds
- Projection with list index correctly reconstructs list structure (previously created Map where List was needed)
- Select validation — invalid Select values and SPECIFIC_ATTRIBUTES without ProjectionExpression rejected
- ConsistentRead on GSI rejected with correct error message
- Limit of 0 rejected with constraint error
- Query/Scan validation ordering matches DynamoDB (input validation before table existence check)
- Expression attribute usage validated syntactically (at parse time) not semantically (at runtime) — fixes false positives with
if_not_existsshort-circuiting - SerializationException pre-checks for non-list field types with DynamoDB-compatible error format
- Error type prefix —
ValidationExceptionusescom.amazon.coral.validate#prefix matching real DynamoDB - BatchExecuteStatement uses short error codes (
ResourceNotFoundnot fully qualified type) and rejects empty Statements array - UpdateTable GSI delete returns
ResourceNotFoundExceptionfor non-existent GSI (previouslyValidationException) - StreamSpecification included in DescribeTable response
- Stack overflow protection: 32-level nesting depth limit on item validation (matches DynamoDB)
- AND/OR short-circuit evaluation in condition expressions
size()function no longer evaluates on invalid attribute types- Idempotency tokens correctly compared in TransactWriteItems
- PutItem no longer double-reads item for conditional checks
- GSI sort key replacement handles all edge cases
- Nested projection preserves document structure (no longer flattens)
- Double-quote identifier escaping in PartiQL
- PartiQL DELETE with missing sort key returns proper error
- PartiQL nested SET paths create correct nested structure (no longer creates literal dot-notation keys)
- PartiQL SELECT with nested map paths resolves correctly
- TTL expiry cleans up LSI entries (previously left orphans)
- GSI/LSI name collision detected and rejected at CreateTable time
- LSI pagination uses composite cursor to handle duplicate sort key values
- ExecuteTransaction breaks on first failure (previously continued executing then rolled back)
- Partition size calculation for ItemCollectionMetrics sums across base table and all LSI tables
- Error message fidelity improvements across empty string, deletion protection, scan segment, and query validation messages
- Local Secondary Indexes (LSI) — full lifecycle: creation, query/scan routing, projection types (ALL, KEYS_ONLY, INCLUDE), sparse index behaviour, write path maintenance across all operations including TTL expiry
- ExecuteTransaction — PartiQL transactional execution with all-or-nothing semantics, condition checks, per-statement cancellation reasons, ConsumedCapacity support
- Parallel Scan — SQLite-level segment filtering via registered FNV-1a scalar function; validated segment/total parameters
- CreateTable extensions —
SSESpecification,TableClass(validated),Tags(inline),DeletionProtectionEnabledwith enforcement on DeleteTable and toggle via UpdateTable - PartiQL WHERE clause extensions —
BETWEEN,IN,CONTAINS,IS MISSING,IS NOT MISSING,OR,NOT, parenthesised grouping - PartiQL nested path projections —
SELECT address.city, tags[0] FROM ...with correct nested structure preservation - PartiQL REMOVE clause —
UPDATE ... REMOVE attribute - PartiQL SET expressions — arithmetic (
count + 1),list_append,if_not_existsin SET clauses - PartiQL IF NOT EXISTS —
INSERT ... VALUE {...} IF NOT EXISTS - PartiQL set literals —
<< 'a', 'b', 'c' >>syntax for SS/NS/BS - PartiQL COUNT(*) and LIMIT support
- Item validation — empty string/set rejection, number precision validation (38 significant digits, ±9.99E+125 range), set deduplication (NS by numeric equivalence)
- Unused expression attribute rejection — unreferenced
ExpressionAttributeNames/ExpressionAttributeValuesentries returnValidationException - ReturnItemCollectionMetrics — partition collection size across base table and all LSI tables
- Per-GSI ConsumedCapacity —
INDEXESmode returns per-GSI breakdown inGlobalSecondaryIndexesmap
- TrackedExpressionAttributes — unified expression resolution with usage tracking; removed duplicate untracked code paths (~400 LOC reduction)
- ScanParams / QueryParams structs replace parameter sprawl in storage layer
- CreateTableMetadata consolidates previously triple-duplicated row mapping
- GSI/LSI secondary indexes on
(base_pk, base_sk)/(table_pk, table_sk)columns — eliminates full table scans during index maintenance - Schema v5 migration with automatic secondary index creation on existing tables
- MCP Server — 33 tools exposing DynamoDB operations for coding agents (Claude Code, Cursor, etc.)
- stdio and Streamable HTTP transports
--read-only,--max-items,--max-size-bytessafety flagsbulk_put_itemstool for batch loading- OneTable
--data-modelintegration with entity-aware agent context and--data-model-summary-limit --mcpflag ondynoxide serveto run MCP alongside HTTP server- Snapshots:
create_snapshot,restore_snapshot,list_snapshots,delete_snapshotwith auto-snapshot beforedelete_table get_database_infotool with data model context
- Import CLI —
dynoxide importfor DynamoDB Export data (JSON Lines format)- Anonymisation rules: fake, mask, hash, redact, null actions
- Cross-table consistency for specified fields
- zstd compression (
--compress) --continue-on-error,--tablesfiltering, atomic--forceoverwrite- Stream-aware import (reproduces source table's StreamSpecification)
- CLI restructuring —
dynoxide serve,dynoxide mcp,dynoxide importsubcommands - Database introspection and port conflict detection on startup
RUST_LOGdebug tracing throughout HTTP and MCP servers
- SQLCipher encryption —
encryptionfeature (vendored OpenSSL via SQLCipher) andencryption-ccfeature (Apple CommonCrypto backend) for encryption at rest - Secure key handling via
--encryption-key-fileorDYNOXIDE_ENCRYPTION_KEYenvironment variable - UpdateTable —
StreamSpecificationsupport, GSI create/delete with backfill - Tag operations —
TagResource,UntagResource,ListTagsOfResource - ReturnValuesOnConditionCheckFailure for TransactWriteItems
- GitHub Action —
nubo-db/dynoxide@v1with optionalsnapshot-urlpreloading - Homebrew formula —
brew install nubo-db/tap/dynoxide - Release CI workflow with cross-platform binary builds (Linux x86_64/aarch64/musl, macOS Intel/Apple Silicon, Windows)
- Private-to-public repo publishing pipeline
- DynamoDBStreams target prefix — server accepts
DynamoDB_20120810.ListStreamsand Streams-prefixed actions From/TryFromconversions for request/response typesitem!macro for ergonomic item construction in tests- Table metadata cache for reduced SQLite round-trips
- Stripped release binaries
nubo-app→nubo-dbGitHub organisation rename
ServerandX-Dynoxide-Versionheaders on all HTTP responsesTableArn,LatestStreamArn, and related ARN fields in API responses- Comprehensive benchmarking suite comparing Dynoxide against DynamoDB Local and LocalStack
- Criterion, iai-callgrind, and custom benchmark binaries
- CI workflows for regression detection and historical tracking
- Standard 13-step workload with JVM warmup protocol
http-serverfeature is now enabled by default- Package renamed to
dynoxide-rsfor crates.io publishing
- Rustdoc warnings
- README version reference
- Core DynamoDB emulator backed by SQLite via
rusqlite - In-memory and persistent database modes
- Table operations: CreateTable, DeleteTable, DescribeTable, ListTables
- Item operations: PutItem, GetItem, DeleteItem, UpdateItem
- Query and Scan with full expression support and pagination
- Batch operations: BatchGetItem, BatchWriteItem
- Transactions: TransactWriteItems, TransactGetItems
- Global Secondary Indexes (GSI)
- DynamoDB Streams (all four view types)
- TTL with background sweep
- Full expression language: KeyCondition, Filter, Condition, Projection, Update
- PartiQL: ExecuteStatement, BatchExecuteStatement
- ReturnConsumedCapacity (TOTAL and INDEXES modes)
- HTTP server (axum-based, DynamoDB JSON wire protocol)
- 300+ tests