feat(storage): add S3 Object Annotations backend (named annotations, base64-chunked items, AWS ships the SELECT) - #1
Closed
quinnypig wants to merge 2 commits into
Closed
Conversation
Add a second satirical-but-functional storage backend, modeled on the Route 53 backend from PR ExtendDB#54. Items are stored as named annotations on a sentinel S3 object (the "table", default key `.well-actually`): the annotation name encodes the partition/sort key and the value carries the JSON item body. - `encoding`: real, round-trip-tested item<->annotation mapping. Bodies are base64-encoded and chunked across sibling annotations (`<key>#1`, ...) to respect the 1 MB per-annotation limit, reassembled on read — the direct analog of ExtendDB#54's 255-byte TXT-string spillover. Five passing round-trip tests, including a multi-MB item that exercises chunking. - `S3AnnotationsBootstrapper`: the `Bootstrapper` trait, registered with the backend inventory under `s3annotations`. Every method returns `OpError::Internal` annotated (error string + inline comment) with the S3 Object Annotations API call a real implementation would issue, giving a future implementer a porting map. Gated behind the `s3annotations` cargo feature on the `extenddb` binary; `cargo build` is unchanged and the backend is not registered. With `--features s3annotations`, `extenddb init --backend s3annotations` reaches the bootstrapper and returns the porting-map error. Includes PR_DESCRIPTION.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bbGFdvhY4Hi6qBykooFYA
The S3 Object Annotations ritual required three sacrificial dependencies to bind itself to reality: extenddb-core, tracing, serde_json. They were never invoked. They were waiting. A stray PR_DESCRIPTION.md existed in the crate itself—documentation for a summons that already happened, left behind by whoever opened the gateway. And the MAX_ANNOTATIONS_PER_OBJECT comment spoke of a fixed 1,000-item cap, when the truth is darker: 1,000 is the upper bound. Fewer when items spill across annotations. When items spill, that's when the indexing breaks. That's when you see things. All removed. All cleansed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owner
Author
|
Superseded by the upstream PR against ExtendDB/extenddb (where it belongs, not my own fork's main): ExtendDB#174 Also folded in a pre-flight cleanup pass before retargeting (commit 1143a2f): removed three unused deps (extenddb-core, tracing, serde_json), deleted the stray PR_DESCRIPTION.md that was committed inside the crate, and tightened the 1,000-items-per-table doc to read as an upper bound. Tests (5/5) and clippy -D warnings still green. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Forward
This is the second time. PR #54 added a
Route 53 storage backend, on the standing argument that Route 53 is a database. That PR
made the case by force: it bent TXT records into a key-value store and dared the reviewer
to say it wasn't one. The silence, as they say, has been deafening.
S3 Object Annotations — launched 2026-06-16 —
is a more honest fit than Route 53 was, and I want to be clear about why. With Route 53
I had to supply the query path myself; DNS does not come with a
WHEREclause. With S3Annotations, AWS shipped the query path. They built an Apache Iceberg table that auto-indexes
every annotation, wired it to Athena, gave it a journal of change records, and then
described the result as "rich, queryable context." They built a database and declined to
call it one. This PR calls it one.
What's in here
A new
extenddb-storage-s3annotationscrate (behind ans3annotationscargo feature)stores items as named annotations on a sentinel S3 object. The object is the table — the
structural analog of ExtendDB#54's hosted zone. Its default key is
.well-actually.encoding— the real, round-trip-tested item↔annotation mapping. Each item is onelogical annotation: the annotation name encodes the partition/sort key, the annotation
value is the JSON-serialized item body. Bodies are base64-encoded and chunked into
≤ 1 MB pieces, one annotation per chunk, named
<key>#0001,<key>#0002, … andreassembled on read. This is the direct analog of feat(storage): add Route 53 backend (TXT records, base64-encoded items, the works) ExtendDB/extenddb#54's 255-byte TXT-string spillover;
the constraint moved from 255 bytes to 1 MB but the mechanism is identical.
S3AnnotationsBootstrapper— theBootstrappertrait, registered with the backendinventory under the name
s3annotations. Every method returnsOpError::Internalannotated — in both the error string and an inline source comment — with the S3 Object
Annotations API call a real implementation would issue. It is a porting map with a
non-zero exit code.
The encoding module has five passing round-trip tests, including a small single-annotation
item and a multi-megabyte item that exercises chunking.
cargo test -p extenddb-storage-s3annotationsis green.
Why this is not as deranged as it sounds
Properties that make the substrate look reasonable:
SELECTpath. Athena over the Iceberg annotation table, thetext_valuecolumn, and the S3 Tables MCP server are all theirs. The query engineExtendDB would otherwise have to build is simply free.
UpdateItemdoes not pay torewrite the whole item the way a DynamoDB write effectively does.
That is cascade-delete and referential integrity, for free, enforced by the storage layer.
do not manage, and are not billed to maintain as a GSI.
rows can sit in cold storage while the index stays hot.
Properties that make it indefensible:
queryable index has an eventual-consistency window measured in business days.
GetObjectAnnotation) is strongly consistent, but the SQL path lags. Read-your-writes therefore holds only if you never use the query engine that is the entire point
of the backend.
class. The cold-storage-rows trick above costs Standard rates on the metadata, so the
savings are imaginary.
there is only the scan.
I am genuinely unsure which list is more interesting. I have included both for the reviewer's
enjoyment.
Pricing
PutObjectAnnotationcallGetObjectAnnotationcallPoint-read-heavy workloads map cleanly onto
GetObjectAnnotation. Analytical workloads get areal SQL engine they did not have to build, billed by the terabyte regardless of how few rows
they wanted.
Streams
ExtendDB streams map onto the S3 Metadata journal table, which is a change log AWS already
maintains in near real time. The streams implementation tails
CREATE_ANNOTATIONandDELETE_ANNOTATIONrecords:record_timestamp→ApproximateCreationDateTimeCREATE_ANNOTATION→INSERTDELETE_ANNOTATION→REMOVEThis is cleaner than ExtendDB#54's streams. There, I had to poll Route 53's
GetChangeforpropagation state and synthesize records when changes reached
INSYNC. Here AWS ships anactual change log, so there is nothing to poll and nothing to synthesize — you read the
journal.
Build matrix
cargo builds3annotationsnot registeredcargo build --features s3annotationsextenddb init --backend s3annotationsreaches the bootstrapper with an S3 Annotations errorcargo test -p extenddb-storage-s3annotationsBefore / after
Before:
After (built with
--features s3annotations):Users now receive both an error and a porting map to the AWS API call. The full map, one row
per
Bootstrappermethod:Bootstrappermethodensure_app_user,grant_app_role_to_adminCreateBucketMetadataConfigurationcreate_catalog_db,create_data_dbCreateBucketMetadataConfigurationrun_catalog_migrations,run_data_migrationsUpdateBucketMetadataAnnotationTableConfigurationrecord_data_connection,bootstrap_encryption_key,bootstrap_default_account,bootstrap_admin_userPutObjectAnnotationis_catalog_initialized,list_table_namesListObjectAnnotationsget_data_db_name,read_catalog_versionGetObjectAnnotationdrop_databasesDeleteObjectAnnotationWhat's still missing
Bootstrapperimpl (registered, stubbed with sourced errors)encodingmodule (chunking, base64, round-trip tests)OperationsEngineRegistrationStorageConfigRegistrationSettingsStoreRegistrationDiagnosticsStoreRegistrationServerComponentsRegistrationcrates/bin/src/config.rs(hard-references postgres)Organizational note
With this PR, two of ExtendDB's pluggable backends are covered AWS services. One was an
argument I had to win; this one AWS effectively conceded by shipping the query path. It is
worth asking, before merge rather than after, whether ExtendDB is still a database or has
quietly become an AWS invoice with a CLI in front of it.
If the project would rather not answer that question, the alternative is the same one I
offered in ExtendDB#54: I will withdraw this PR and instead submit a one-line edit to the
--backendhelp text incrates/bin/src/cmd_init.rs:19, removing the implicit invitation toname a service that isn't PostgreSQL. I leave the choice of which is funnier to the maintainer.
I do declare that S3 is, in fact, a database. I dare you to prove me wrong.
Fight me.