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_app defines a supervised repository that owns region, endpoint, credentials, retry policy and a Task.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) and Dynamo.Encoder/Decoder are removed.
  • Typed fields – field :name, :type, opts. 16 built-in types incl. :utc_datetime, :decimal, :binary, sets; Dynamo.Type behaviour 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 (correct Host with port), jittered exponential retry for retryable errors, :telemetry events, 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 both message/Message are read, so :resource_not_found, :conditional_check_failed, :provisioned_throughput_exceeded, :transaction_canceled (with cancellation_reasons) etc. actually match.
  • Dynamo.Page – uniform result for query/scan with last_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 every condition:/filter: option; generates names/values with the field's type.
  • Embedded schemas – use Dynamo.Schema, embedded: true; every schema is a Dynamo.Type, so field :address, MyApp.Address and field :items, {:list, MyApp.Item} round-trip with full typing.
  • {:list, type} parameterised lists.
  • Vector search – {:vector, n} field type, vector_index in the schema DSL (distance, projection, partition key, inline filters), Repo.search_vectors/4, create_table --schema emits VectorIndexes, and the client routes SearchVectors to AWS's separate search endpoint.
  • load/2 recovers key fields from pk/sk when a projection omitted them (keys-only GSIs, vector indexes).
  • entity_name ParentModule – compile-time link for parent/child items.
  • table/1 accepts compile-time expressions.
  • mix dynamo.create_table --schema A --schema B merges 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-15 with - became 2024-15). Keys are now built from values directly; separator collisions are rejected.
  • Composite partition key fields declared with field ..., partition_key: true were emitted in reverse order.
  • Nil partition-key fields silently became the string "empty"; now a :validation_error.
  • AWS errors were never categorised (namespaced __type, lowercase message).
  • Transaction update operators produced invalid expressions (SET a + :v) and expression attributes passed as documented were dropped.
  • belongs_to keys were applied in get/query but not delete/update/batch get/transactions. belongs_to is removed; use entity_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 in Dynamo.Raw.
  • Binaries were typed S or B depending on UTF-8 validity; :binary is now explicit and stable.
  • :client was 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, UnprocessedItems retried with backoff.
  • list_items could not paginate; query/2 returns %Dynamo.Page{}.
  • Query accumulation was O(n²) and unbounded; streams are lazy.
  • "Parallel" streaming was sequential Stream.concat; scan_to_process used an un-awaited linked Task.async and mis-wrapped batch messages; producer_spec/2 called a non-existent function. All replaced by Dynamo.Stream.
  • Dynamo.Logger wrote 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_schema produced schemas that could not read the source table. Fixed; create_table --schema derives keys and GSIs from a schema.
  • lib/examples.ex shipped example schemas in the library namespace; removed.
  • Tests: table_stream_test.exs did not compile; mock/meck removed in favour of Req.Test plugs and an in-memory fake; property-based round-trip tests for all types; golden tests for every request builder.

Migration

0.10.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: :prefixentity_name P + field :kind, :string, sort_key: true, default: "child"
Dynamo.Config.put_process_config/1repo config / use Dynamo.Schema, opts
Dynamo.Logger.enable/0attach 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.