Skip to content

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

Merged
PMerlet merged 32 commits into
mainfrom
feature/prd-1670-dynamodb-datasource-for-the-nodejs-agent-parity-with-cosmos
Oct 28, 2026
Merged

PMerlet merged 32 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

  • Adds @forestadmin/datasource-dynamodb in packages/datasource-dynamodb/src. It supports table introspection (sampling up to 100 items, depth 5), a manual-schema mode, a chainable collection builder, and virtual "array collections" that expose array elements as their own collections.
  • Plans reads as table queries, GSI queries, scans, or BatchGet key lookups, and pushes filters to DynamoDB where possible; the rest is processed in memory. Sorting, pagination (with an offset cache), aggregations, and binary/set/date value handling are included.
  • Native queries run PartiQL SELECT statements only, with maxScannedItems enforcement and a 1,000-page cap. Default client retries are 10 attempts with adaptive mode.
  • Includes a DynamoDB Local integration suite via docker-compose.yml and the build workflow matrix entry.
  • Risk: introspection version is hardcoded to 1 in types.ts and rejected otherwise by validateManualSchema in manual-schema.ts; new packages land as a first release, so breaking changes can still be made freely before adoption.

Macroscope summarized c6eb113.

@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

75 new issues

Tool Category Rule Count
qlty Structure Function with high complexity (count = 16): constructor 26
qlty Structure Function with many parameters (count = 4): aggregate 16
qlty Structure Function with many returns (count = 5): computeAggregation 14
qlty Structure Deeply nested control flow (level = 4) 9
qlty Structure Complex binary expression 6
qlty Structure High total complexity (count = 154) 4

@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 (24)

RatingFile% DiffUncovered Line #s
New file Coverage rating: A
packages/datasource-dynamodb/src/schema/manual-schema.ts97.1%40, 278-279
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, 507-508, 741
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/types.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.ts96.0%415, 438, 470-472...
New file Coverage rating: A
packages/datasource-dynamodb/src/utils/serializer.ts96.7%138-139
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 415, 438, 470-472, 493-495, 521, 576, 588, and 610
- `packages/datasource-dynamodb/src/collection.ts` -- Lines 101-102, 507-508, and 741
- `packages/datasource-dynamodb/src/datasource.ts` -- Line 89
- `packages/datasource-dynamodb/src/schema/manual-schema.ts` -- Lines 40 and 278-279
- `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 138-139
- `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.

@PMerlet
PMerlet marked this pull request as ready for review October 16, 2026 18:57
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 3 commits October 22, 2026 10:50
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
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>
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>
…ts path

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… 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.

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>
…riting an empty set

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
PMerlet and others added 4 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>
…e @@@ separator

The AWS SDK clients are dependencies instead of peer dependencies. Nested maps are
columns named with @@@, which Forest shows as a path. The builder adjusts the tables it
names and the other tables are still introspected. The introspection option and the
introspect function replace the schema and disableIntrospection options, as in the other
datasources. An attribute an item does not have is read as null, so Present and Blank
tell it apart from a value.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
PMerlet and others added 2 commits October 27, 2026 18:13
…evaluates itself

A column offers the operators DynamoDB evaluates and only the sort keys sort. The elements of
an array collection only filter on the keys of the item and their position. inMemoryProcessing
brings the rest back. Present is evaluated by DynamoDB as the negation of Blank.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…y elements as such

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… element in one statement

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

@arnaud-moncel arnaud-moncel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's see for a v1

@PMerlet
PMerlet merged commit b15b168 into main Oct 28, 2026
49 checks passed
@PMerlet
PMerlet deleted the feature/prd-1670-dynamodb-datasource-for-the-nodejs-agent-parity-with-cosmos branch October 28, 2026 15:35
forest-bot added a commit that referenced this pull request Oct 28, 2026
# @forestadmin/datasource-dynamodb 1.0.0 (2026-10-28)

### Features

* **datasource-dynamodb:** add a DynamoDB datasource ([#2023](#2023)) ([b15b168](b15b168))
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