View Source Changelog
0.2.0 – unreleased
Complete rewrite. Every public API changed. The 0.1 line was a beta whose documented API did not match its implementation; 0.2 is what the docs promised.
Architecture
Dynamo.Repo–use Dynamo.Repo, otp_app: :my_appdefines a supervised repository that owns region, endpoint, credentials, retry policy and aTask.Supervisor. All operations are called on the repo.Dynamo.Table,Dynamo.AWS,Dynamo.DynamoDB,Dynamo.Config,Dynamo.Logger,Dynamo.Helper,Dynamo.ErrorHandler,Dynamo.Transaction,Dynamo.Table.Stream(.Producer)andDynamo.Encoder/Decoderare removed.- Typed fields –
field :name, :type, opts. 16 built-in types incl.:utc_datetime,:decimal,:binary, sets;Dynamo.Typebehaviour for custom types. Encoding/decoding is schema-driven and round-trips. Dynamo.Request– pure request builders returning{action, payload}, unit-tested against the DynamoDB JSON shapes.Dynamo.Client– SigV4 (correctHostwith port), jittered exponential retry for retryable errors,:telemetryevents, error normalisation.Dynamo.Credentials– static → env → ECS → IMDSv2 chain with a supervised, single-flight refreshing cache.Dynamo.Error– the only error type. AWS__types are un-namespaced and bothmessage/Messageare read, so:resource_not_found,:conditional_check_failed,:provisioned_throughput_exceeded,:transaction_canceled(withcancellation_reasons) etc. actually match.Dynamo.Page– uniform result forquery/scanwithlast_evaluated_key;Enumerable.Dynamo.Stream– lazy query/scan;segments:runs a genuinely parallel scan with bounded concurrency and backpressure.Dynamo.Update– SET/REMOVE/ADD/DELETE with{:increment, n},{:decrement, n},{:add, _},{:delete, set},{:append, l},{:prepend, l},{:if_not_exists, v},:remove.Dynamo.Condition– keyword/combinator DSL accepted by everycondition:/filter:option; generates names/values with the field's type.- Embedded schemas –
use Dynamo.Schema, embedded: true; every schema is aDynamo.Type, sofield :address, MyApp.Addressandfield :items, {:list, MyApp.Item}round-trip with full typing. {:list, type}parameterised lists.- Vector search –
{:vector, n}field type,vector_indexin the schema DSL (distance, projection, partition key, inline filters),Repo.search_vectors/4,create_table --schemaemitsVectorIndexes, and the client routesSearchVectorsto AWS's separate search endpoint. load/2recovers key fields frompk/skwhen a projection omitted them (keys-only GSIs, vector indexes).entity_name ParentModule– compile-time link for parent/child items.table/1accepts compile-time expressions.mix dynamo.create_table --schema A --schema Bmerges GSIs of a single-table design.- Credentials cache read path is lock-free (
:persistent_term); only refreshes go through the GenServer. - CI workflow (unit matrix + live suite against DynamoDB Local).
Bugs fixed (from the 0.1 review)
- Composite sort keys were corrupted when a value contained the separator
(
2024-01-15with-became2024-15). Keys are now built from values directly; separator collisions are rejected. - Composite partition key fields declared with
field ..., partition_key: truewere emitted in reverse order. - Nil partition-key fields silently became the string
"empty"; now a:validation_error. - AWS errors were never categorised (namespaced
__type, lowercasemessage). - Transaction update operators produced invalid expressions (
SET a + :v) and expression attributes passed as documented were dropped. belongs_tokeys were applied in get/query but not delete/update/batch get/transactions.belongs_tois removed; useentity_name ParentModule(resolved at compile time) plus a typed sort key with a default prefix (see guides/DATA_MODELLING.md).- Encoder/decoder treated any map with a key like
"S"/"N"/"M"as an already-encoded value, corrupting user data both ways. Pre-encoded values must now be wrapped inDynamo.Raw. - Binaries were typed
SorBdepending on UTF-8 validity;:binaryis now explicit and stable. :clientwas ignored by query/batch write/parallel scan/streams; the repo owns the client so every call uses the same configuration.- ETS credential cache was created lazily by arbitrary processes (race, destroyed on owner exit); replaced by a supervised GenServer.
- Batch writes reported total failure as
{:ok, processed_items: 0}; hard errors are now returned,UnprocessedItemsretried with backoff. list_itemscould not paginate;query/2returns%Dynamo.Page{}.- Query accumulation was O(n²) and unbounded; streams are lazy.
- "Parallel" streaming was sequential
Stream.concat;scan_to_processused an un-awaited linkedTask.asyncand mis-wrapped batch messages;producer_spec/2called a non-existent function. All replaced byDynamo.Stream. Dynamo.Loggerwrote full item payloads to stdout on success only; replaced by telemetry.- Mix tasks did not load the host app config, exited 0 on failure, and
generate_schemaproduced schemas that could not read the source table. Fixed;create_table --schemaderives keys and GSIs from a schema. lib/examples.exshipped example schemas in the library namespace; removed.- Tests:
table_stream_test.exsdid not compile;mock/meck removed in favour ofReq.Testplugs and an in-memory fake; property-based round-trip tests for all types; golden tests for every request builder.
Migration
| 0.1 | 0.2 |
|---|---|
Dynamo.Table.put_item(s) | MyRepo.put(s) |
Dynamo.Table.get_item(s) | MyRepo.get(s) (full key required) |
Dynamo.Table.list_items(s, sk_operator: :begins_with, sort_key: v) | MyRepo.query(s, sort_key: {:begins_with, v}) → %Dynamo.Page{} |
Dynamo.Table.list_items(s, index_name: "X") | MyRepo.query(s, index: "X") |
Dynamo.Table.update_item(s, %{a: 1}) | MyRepo.update(s, a: 1) |
Dynamo.Table.batch_write_item(list) | MyRepo.batch_write(list) |
Dynamo.Table.parallel_scan(M, segments: 4) | MyRepo.stream(M, segments: 4) |
Dynamo.Table.Stream.scan(M) | MyRepo.stream(M) |
Dynamo.Transaction.transact([...]) | MyRepo.transaction([...]) (ops now {:put, s, opts} etc.) |
table_name "t" | table "t" |
field :x (untyped) | field :x, :string (or the real type) |
belongs_to :parent, P, sk_strategy: :prefix | entity_name P + field :kind, :string, sort_key: true, default: "child" |
Dynamo.Config.put_process_config/1 | repo config / use Dynamo.Schema, opts |
Dynamo.Logger.enable/0 | attach to [:dynamo, :request, :stop] |
Stored keys are compatible for schemas that used the default # separator,
single-field keys, default pk/sk names and single-word module names.
The default entity name is now Macro.underscore/1 of the module basename
(UserProfile → "user_profile"); 0.1 lower-cased it ("userprofile").
Set entity_name "userprofile" explicitly on such schemas to keep reading
existing data. Composite sort keys written by 0.1 with values containing the
separator are corrupted and must be re-written.
0.1.x
Initial beta.