Skip to content

feat(datasource-dynamodb): add a DynamoDB datasource - #2023

Open
PMerlet wants to merge 28 commits into
mainfrom
feature/prd-1670-dynamodb-datasource-for-the-nodejs-agent-parity-with-cosmos
Open

PMerlet wants to merge 28 commits into
mainfrom
feature/prd-1670-dynamodb-datasource-for-the-nodejs-agent-parity-with-cosmos

Conversation

@PMerlet

@PMerlet PMerlet commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

fixes PRD-1670

Adds @forestadmin/datasource-dynamodb: a datasource for Amazon DynamoDB with the capabilities of the Cosmos DB one (introspection, manual schema, composite keys, relations, search, aggregation, charts, arrays as collections, native queries) plus single-table design. DynamoDB cannot sort, count or aggregate server-side, so the datasource reads by key whenever a filter allows it and does the rest on the items it reads; scanPolicy (warn by default) and maxScannedItems keep a request that no key serves from reading a whole table unnoticed.

What changes

  1. Before: a DynamoDB table could not be used as a datasource. After: createDynamoDBDataSource({ region }) introspects the tables (keys and indexes from DescribeTable, attributes from a sample), or takes a manual schema; the partition and sort keys are the composite primary key.
  2. Before: n/a. After: each request takes the cheapest path: Query on the table or an index when an equality pins a partition key, BatchGetItem when the filter names whole keys (what relation emulation sends), parallel Scan otherwise. Operators DynamoDB lacks (ILike, EndsWith, date intervals), sorts on non-key attributes, counts, aggregations and charts are computed on the items read, streamed so memory stays flat.
  3. Before: one collection per table. After: several collections can share a table by sort key prefix or discriminator attribute, and an array of maps can be exposed as a collection keyed by its parent and its index.
  4. Before: the CI knows no DynamoDB. After: datasource-dynamodb joins the test matrix, whose job starts DynamoDB Local (docker-compose.yml in the package) and sets DYNAMODB_ENDPOINT for that package only.

Tests

Unit tests use a fake client and assert the requests sent. An integration suite (22 tests, skipped without DYNAMODB_ENDPOINT) runs against DynamoDB Local, since a fake cannot tell whether DynamoDB accepts an expression: it is what validates the key-attribute and sort-key rules of Query filters, the nested array writes and the fan-out of an In on a partition key. A real agent over seeded DynamoDB Local data was also exercised by hand (list, count, paging, search, relations, chart, writes).

Not delivered, per the ticket's own list of additions: collections backed by a secondary index, and optimistic updates.

Decisions

  • PRD-1670: 8 functional, 2 architectural · 7 unticked

Definition of Done

General

  • Write an explicit title for the Pull Request, following Conventional Commits specification
  • Test manually the implemented changes
  • Validate the code quality (indentation, syntax, style, simplicity, readability)

Security

  • Consider the security impact of the changes made

Squash message

feat(datasource-dynamodb): add a DynamoDB datasource

DynamoDB reads fast by key and scans otherwise, and cannot sort, count or aggregate on the server. The datasource reads by key whenever a filter allows it (Query, a Query per partition of an In, BatchGetItem, parallel Scan otherwise) and does the rest on the items it reads. It introspects tables or takes a manual schema, exposes the keys as a composite primary key, shares a table between collections, exposes arrays of maps (nested too) as collections, and runs PartiQL SELECT native queries. scanPolicy and maxScannedItems keep a request that no key serves from reading a table unnoticed.

🤖 Generated with Claude Code

Note

Add a DynamoDB datasource package with introspection and virtual array collections

  • Adds a new datasource-dynamodb package with createDynamoDBDataSource, supporting manual schemas, builder-based table registration, and live introspection of tables and indexes (index.ts).
  • Reads choose between Query, Scan, secondary indexes, and BatchGetItem based on filter shape, with key-type coercion and partition-key In fan-out across parallel queries (access-path.ts).
  • Includes a StreamingAggregator for count/sum/avg/min/max with timezone-aware date grouping, a pagination-token cache with bounded memory and TTL, and scan limits via maxScannedItems and maxOffset (executor.ts).
  • Supports single-table entity discriminators, virtual array collections exposed as nested reads, and read-only PartiQL native queries with parameterization and page/item limits (datasource.ts).
  • Ships DynamoDB Local docker-compose setup, a CI matrix entry, and extensive unit plus integration test suites.

Macroscope summarized 519b61f.

@linear-code

linear-code Bot commented Oct 16, 2026

Copy link
Copy Markdown

PRD-1670

@qltysh

qltysh Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

70 new issues

Tool Category Rule Count
qlty Structure Function with high complexity (count = 28): readRecords 24
qlty Structure Function with many parameters (count = 4): aggregate 14
qlty Structure Function with many returns (count = 5): computeAggregation 14
qlty Structure Deeply nested control flow (level = 4) 8
qlty Structure Complex binary expression 6
qlty Structure High total complexity (count = 129) 4

Comment thread packages/datasource-dynamodb/src/array-collection.ts Outdated
Comment thread packages/datasource-dynamodb/src/array-collection.ts Outdated
Comment thread packages/datasource-dynamodb/src/array-collection.ts Outdated
Comment thread packages/datasource-dynamodb/src/array-collection.ts Outdated
Comment thread packages/datasource-dynamodb/src/collection.ts
Comment thread packages/datasource-dynamodb/src/utils/access-path.ts Outdated
Comment thread packages/datasource-dynamodb/src/introspection/introspector.ts Outdated
Comment thread packages/datasource-dynamodb/src/utils/access-path.ts Outdated
Comment thread packages/datasource-dynamodb/src/utils/access-path.ts Outdated
Comment thread packages/datasource-dynamodb/src/schema/manual-schema.ts Outdated
Comment thread packages/datasource-dynamodb/src/utils/executor.ts
Comment thread packages/datasource-dynamodb/src/utils/expression-builder.ts Outdated
Comment thread packages/datasource-dynamodb/src/utils/sort.ts Outdated
Comment thread packages/datasource-dynamodb/src/virtual-collections.ts Outdated
@qltysh

qltysh Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Qlty


Coverage Impact

This PR will not change total coverage.

Modified Files with Diff Coverage (23)

RatingFile% DiffUncovered Line #s
New file Coverage rating: A
packages/datasource-dynamodb/src/schema/manual-schema.ts97.4%39, 79
New file Coverage rating: A
packages/datasource-dynamodb/src/utils/sort.ts100.0%
New file Coverage rating: A
packages/datasource-dynamodb/src/utils/concurrency.ts100.0%
New file Coverage rating: A
packages/datasource-dynamodb/src/collection.ts98.4%101-102, 478-479, 709
New file Coverage rating: A
packages/datasource-dynamodb/src/utils/aggregator.ts97.4%29, 35
New file Coverage rating: A
packages/datasource-dynamodb/src/datasource.ts97.1%89
New file Coverage rating: A
packages/datasource-dynamodb/src/introspection/introspector.ts100.0%
New file Coverage rating: A
packages/datasource-dynamodb/src/errors.ts100.0%
New file Coverage rating: A
packages/datasource-dynamodb/src/utils/type-converter.ts97.6%72
New file Coverage rating: A
packages/datasource-dynamodb/src/virtual-collections.ts100.0%
New file Coverage rating: A
packages/datasource-dynamodb/src/utils/executor.ts100.0%
New file Coverage rating: A
packages/datasource-dynamodb/src/client.ts100.0%
New file Coverage rating: A
packages/datasource-dynamodb/src/utils/partiql.ts100.0%
New file Coverage rating: A
packages/datasource-dynamodb/src/settings.ts100.0%
New file Coverage rating: A
packages/datasource-dynamodb/src/utils/pagination-cache.ts96.6%67
New file Coverage rating: A
packages/datasource-dynamodb/src/index.ts100.0%
New file Coverage rating: A
packages/datasource-dynamodb/src/utils/access-path.ts98.3%136, 162
New file Coverage rating: A
packages/datasource-dynamodb/src/array-collection.ts95.6%374, 397, 429-431...
New file Coverage rating: A
packages/datasource-dynamodb/src/utils/serializer.ts96.6%135-136
New file Coverage rating: A
packages/datasource-dynamodb/src/utils/compare.ts100.0%
New file Coverage rating: A
packages/datasource-dynamodb/src/utils/expression-builder.ts100.0%
New file Coverage rating: A
packages/datasource-dynamodb/src/introspection/builder.ts100.0%
New file Coverage rating: A
packages/datasource-dynamodb/src/schema/schema-converter.ts100.0%
Total98.2%
🤖 Increase coverage with AI coding...
In the `feature/prd-1670-dynamodb-datasource-for-the-nodejs-agent-parity-with-cosmos` branch, add test coverage for this new code:

- `packages/datasource-dynamodb/src/array-collection.ts` -- Lines 374, 397, 429-431, 452-454, 480, 535, 544, and 566
- `packages/datasource-dynamodb/src/collection.ts` -- Lines 101-102, 478-479, and 709
- `packages/datasource-dynamodb/src/datasource.ts` -- Line 89
- `packages/datasource-dynamodb/src/schema/manual-schema.ts` -- Lines 39 and 79
- `packages/datasource-dynamodb/src/utils/access-path.ts` -- Lines 136 and 162
- `packages/datasource-dynamodb/src/utils/aggregator.ts` -- Lines 29 and 35
- `packages/datasource-dynamodb/src/utils/pagination-cache.ts` -- Line 67
- `packages/datasource-dynamodb/src/utils/serializer.ts` -- Line 135-136
- `packages/datasource-dynamodb/src/utils/type-converter.ts` -- Line 72

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

Comment thread packages/datasource-dynamodb/src/schema/manual-schema.ts Outdated
Comment thread packages/datasource-dynamodb/src/collection.ts Outdated
Comment thread packages/datasource-dynamodb/src/introspection/introspector.ts
Comment thread packages/datasource-dynamodb/src/collection.ts Outdated
Comment thread packages/datasource-dynamodb/src/utils/access-path.ts
Comment thread packages/datasource-dynamodb/src/utils/compare.ts Outdated
Comment thread packages/datasource-dynamodb/src/array-collection.ts
Comment thread packages/datasource-dynamodb/src/utils/expression-builder.ts Outdated
Comment thread packages/datasource-dynamodb/src/array-collection.ts Outdated
Comment thread packages/datasource-dynamodb/src/introspection/introspector.ts
Comment thread packages/datasource-dynamodb/src/array-collection.ts
Comment thread packages/datasource-dynamodb/src/collection.ts Outdated
Comment thread packages/datasource-dynamodb/src/utils/access-path.ts
Comment thread packages/datasource-dynamodb/src/collection.ts
Comment thread packages/datasource-dynamodb/src/utils/access-path.ts
Comment thread packages/datasource-dynamodb/src/utils/serializer.ts Outdated
Comment thread packages/datasource-dynamodb/src/collection.ts
Comment thread packages/datasource-dynamodb/src/utils/aggregator.ts Outdated
Comment thread packages/datasource-dynamodb/src/utils/aggregator.ts Outdated
@PMerlet
PMerlet marked this pull request as ready for review October 16, 2026 18:57
Comment thread packages/datasource-dynamodb/src/array-collection.ts
Comment thread packages/datasource-dynamodb/src/collection.ts
PMerlet and others added 14 commits October 22, 2026 10:50
DynamoDB reads fast by key and scans otherwise, and it cannot sort, count or
aggregate server-side. The datasource picks the cheapest access path for each
request (Query on the table or an index, BatchGetItem for whole keys, parallel
Scan otherwise) and does the rest on the items it reads: operators DynamoDB
lacks, sorting on non-key attributes, aggregations and charts.

It introspects tables or takes a manual schema, exposes the partition and sort
keys as a composite primary key, shares one table between collections
(single-table design), exposes arrays of maps as collections, and runs PartiQL
SELECT native queries. `scanPolicy` and `maxScannedItems` keep a request that
no key serves from reading a whole table unnoticed.

The unit tests use a fake client; an integration suite runs against DynamoDB
Local, started by the CI like the other datasources' databases.

Refs: PRD-1670

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The package pinned a toolkit version older than the workspace's, so yarn
installed the published one next to it and its error classes came out
untyped in the build.

Refs: PRD-1670

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Refs: PRD-1670

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The ticket listed nested arrays among the virtual collections, and the first
version refused them. A collection can now have an array collection as its
parent: its records are keyed by the table key and the position in each array,
and writes address the element through the whole path. An array held in a map
(a->b) is exposed too.

Refs: PRD-1670

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Refs: PRD-1670

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- an index that leaves out items without its sort key is only read with a condition on that key
  or when declared to cover every item
- key lookups drop nested OR arms and duplicate keys, which BatchGetItem refuses
- writes: caches are dropped after a partial failure, in-flight writes finish before the error
  is raised, a record cannot claim another entity, array writes stay within DynamoDB's expression
  size and the entity of their parent, and an update under a missing map falls back to rewriting
- pagination positions are kept per timezone, Present is evaluated like the toolkit does,
  date-times sort and aggregate by instant, and a large client-side sort no longer overflows
- native queries leave placeholders inside literals alone and bound the pages they follow
- introspection accepts attributes named like Object.prototype members
- nested array collections use the item schema their parent declares

Refs: PRD-1670

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…date comparison

Refs: PRD-1670

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…and warn less

Running a real agent over DynamoDB showed that a filter through a relation sends
an In on the partition key, which was scanned: each value is now read with a
Query of its own. The same run showed a scan warning on every first page of an
unfiltered collection, which reads one page: it is no longer warned about or
denied, and the same warning is repeated at most once a minute.

Refs: PRD-1670

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Refs: PRD-1670

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- a map seen empty in some items is no longer both a column and its nested columns
- a field of a manual schema cannot change the type of a key
- a key lookup never builds the product of long lists, and lists each value once
- date-only strings are compared by instant too
- Before and After are evaluated on the items, since DynamoDB compares dates as text

Refs: PRD-1670

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- chart caches are shared by every collection of a table and hold copies
- array collections refuse key columns with the same name, create and update
  under a map the item lacks by rewriting it, and replace what a patch names
- attributes named with the path separator are refused, not silently merged
- a create returns the discriminator it stored, a first page of an entity
  collection no longer bypasses scanPolicy, and a reversed range is not a BETWEEN

Refs: PRD-1670

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…read strongly before rewriting

Refs: PRD-1670

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Refs: PRD-1670

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Refs: PRD-1670

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
PMerlet and others added 5 commits October 22, 2026 10:50
…elements needs

A real run over 100 000 items showed the first page of an array collection reading
the whole table. Without a sort, parents are now read in batches of 100 until the page
is filled.

Refs: PRD-1670

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ged keys on update

Forest greys the primary key fields of the creation form, so a string sort key left
empty now gets a UUID (behind the entity prefix, if any), like the partition key.
Setting a many-to-one relation writes its foreign key, which can be a key of the
table: writing the value a key already has is a no-op, changing it is still refused.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ocal

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…allback and share the parent read budget

A map written through the client-side rewrite of an update was merged with the old one,
keeping keys the SET would have dropped: each entry now replaces what is at its path.
The pages of parents an array collection reads were each given a budget of their own,
so a selective read could go past maxScannedItems: the parents read now count together.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@PMerlet
PMerlet force-pushed the feature/prd-1670-dynamodb-datasource-for-the-nodejs-agent-parity-with-cosmos branch from a27f12d to 7f68052 Compare October 22, 2026 08:50
Comment thread packages/datasource-dynamodb/src/utils/type-converter.ts Outdated
Comment thread packages/datasource-dynamodb/src/utils/expression-builder.ts
Comment thread packages/datasource-dynamodb/src/utils/type-converter.ts Outdated
Comment thread packages/datasource-dynamodb/src/collection.ts
Comment thread packages/datasource-dynamodb/src/collection.ts
if (entity) conditions.push(entity);

try {
await this.root.executor.put({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/array-collection.ts:557

The rewrite fallbacks can erase successful concurrent writes, including appends and updates to unrelated parent attributes. replaceElements and appendByRewriting put the entire item snapshot read by get, while their conditions only check item existence and entity membership; because PutItem replaces the whole item, changes made after the read are lost. Restrict writes to the affected path or condition the replacement on the snapshot and retry on conflict.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/datasource-dynamodb/src/array-collection.ts around line 557:

The rewrite fallbacks can erase successful concurrent writes, including appends and updates to unrelated parent attributes. `replaceElements` and `appendByRewriting` put the entire item snapshot read by `get`, while their conditions only check item existence and entity membership; because `PutItem` replaces the whole item, changes made after the read are lost. Restrict writes to the affected path or condition the replacement on the snapshot and retry on conflict.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Sonnet 5.5 (claude-sonnet-5-5): Not fixed here: a rewrite fallback can overwrite a write that lands between its read and its put, and closing that needs optimistic locking on a version attribute, which is a feature of its own.

Why

The read is strongly consistent and the put is conditioned on the item existing, so only a concurrent write inside that window is lost. Tracked in PRD-1673, which covers these fallbacks and replaceOne.

…mits on every read

timeonly is published as Time and GeoJSON points as Json, the types whose values the
toolkit validates. Binary attributes are filtered on the returned items, since DynamoDB
would compare them to text. BatchGetItem lookups count against maxScannedItems and a
skip beyond maxOffset is refused whichever way the page is read.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Comment thread packages/datasource-dynamodb/src/collection.ts Outdated
Forest sends a binary attribute as base64 text: create and update decode it, so the item
holds a DynamoDB B attribute again instead of the text.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Comment thread packages/datasource-dynamodb/src/collection.ts Outdated
…ts path

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Comment thread packages/datasource-dynamodb/src/collection.ts Outdated
… of the schema

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

@PMerlet PMerlet left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spec (PRD-1670): 7 unvalidated decisions: not final. One option name differs from the ticket, see the finding on index.ts.

Comment thread packages/datasource-dynamodb/src/client.ts
Comment thread packages/datasource-dynamodb/src/collection.ts
Comment thread packages/datasource-dynamodb/src/schema/manual-schema.ts Outdated
Comment thread packages/datasource-dynamodb/src/index.ts
Comment thread packages/datasource-dynamodb/src/collection.ts
Comment thread packages/datasource-dynamodb/src/datasource.ts
A number beyond 2^53 is read as text, string and number sets are written back as sets, a
manual schema collection may omit its fields, virtualArrayCollections is read as an alias,
an unknown scanPolicy is refused, and the SELECT-only rule of native queries is tested.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Comment thread packages/datasource-dynamodb/src/collection.ts Outdated
…riting an empty set

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Comment thread packages/datasource-dynamodb/src/collection.ts
PMerlet and others added 3 commits October 22, 2026 11:58
… the cache

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rtual collections

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ion tests

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant