All notable changes to this project are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
0.2.2 - 2026-08-01
Evidence. Every number this file publishes is now produced by a script in the
repository, the supported TypeDB floor was measured instead of assumed, and the
tests that found the three fixes below did not exist a release ago. No public
API changed — test/api_snapshot.txt is byte for byte the one 0.2.1 shipped.
Added
- A bounded concurrency soak in the integration suite: 200 concurrent reads, 100 concurrent writes checked for exactly-once landing, and 25 concurrent transactions. The numbers the CHANGELOG has published since 0.1.0 were produced by hand; these run on every push.
- A coverage floor, enforced by CI.
- A fault-injection matrix: thirteen ways an adapter or a server can misbehave,
against every public call that reaches one, asserting that each produces a
%TypeDB.Error{}and leaves the connection alive. - Property-based round-trip tests over the wire boundary —
TypeDB.Duration,TypeDB.DateTimeTZ,TypeDB.GivenandTypeDB.Concept— where every subtle bug in this driver has actually been.stream_datais a test-only dependency and does not reach the package. bench/decode.exsandbench/transport.exs, the scripts behind the numbers quoted here. They were previously run by hand from a scratch directory that no longer exists, which made every figure in this file unfalsifiable.
Changed
- The supported TypeDB floor is 3.12.0, and it is now a measured one. The
suite was run down the published releases rather than reasoned about: 3.12.0
passes whole, 3.11.5 fails fourteen integration tests, and 3.5.0 fails more
broadly still. TypeQL's
givenstage — the driver's answer to query injection — is a syntax error before 3.12, so every parameterised query fails there, andUser.delete/2on an unknown user answers 400 rather than 404. 3.12.0 is in CI's integration matrix beside 3.12.1 andlatest, so the claim stays true. The README said "3.12 or newer" before this and happened to be right; it was not evidence. - Re-measured throughput, on the scripts now in
bench/, against a local TypeDB in this project's container — 400 requests per run, warm pool, a one-rowmatch. At 200-way concurrency Finch sustains ~1820 req/s and Req ~1640, both with a p99 under 130ms;:httpcmanages ~375 req/s at a p99 of 685ms. The README's table is these numbers, and now names the script that produced them. 0.1.0 published 77 req/s for:httpcat 200-way, which did not reproduce; the ratio is the finding and it holds, the absolute numbers are a property of whatever machine you run them on.
Fixed
TypeDB.Transaction.analyze/3returned{:ok, :ok}for a 200 with an empty body, where its spec promises{:ok, map()}. It now rejects a payload that is not a structure. Found by the fault matrix. (analyze/3's return is the one documented SemVer exemption, which is why this is a patch.)TypeDB.Duration.parse/1ran a regular expression once per component, and its trailing(.*)copied the rest of the string each time. It cost 16µs a duration where every other cast costs under half a microsecond. Scanning the number's length and slicing brings it to 1.6µs — ten times faster, for the same values: checked by re-parsing 200,000 generated durations, well-formed and malformed, through both implementations.TypeDB.Concept.cast/2askedCode.ensure_loaded?(Decimal)once per value. For an application that hasDecimalthat is a cached lookup costing nothing; for one that does not it is a code-server round trip, and it was the entire cost of casting a decimal — 22µs a value, against 0.4µs with the dependency present. The answer is now memoised in:persistent_term, which takes 50,000 casts withoutDecimalfrom 1099ms to 5ms.
0.2.1 - 2026-08-01
Documentation only. The single change under lib/ is six lines of moduledoc;
test/api_snapshot.txt is byte for byte the one 0.2.0 shipped.
These were written after 0.2.0 was tagged and were briefly listed under it in this file, which was wrong: a published release does not grow.
Added
- Four guides, published with the docs and shipped in the package: Transactions, Errors and retries, Telemetry and logging, and Testing an application.
- A Livebook notebook, with a Run in Livebook badge on the README: a database, a schema, reads and writes, a parameterised query that survives a hostile value, and a transaction. Its code blocks are parsed by the test suite, and the version it installs is checked against this project.
- A Limitations section in the README: answers arrive whole, a connection points at one server, retries block the caller, one connection is one HTTP pool.
Changed
- The documentation's module groups are ordered for reading rather than by accretion, and a test now asserts every published module is filed under exactly one of them.
0.2.0 - 2026-08-01
Work towards 1.0. Retry and timeout behaviour, observability, and the shape of the public surface — the three things 1.0 makes irreversible.
Upgrading from 0.1.0
Four changes can be noticed by working code, and each is deliberate:
- Backoff delays are now random within their bound. A test that asserted an
exact wait should assert the bound, or pass a function to
:retry_backoff. - More requests are retried — reads,
rollback,close,Database.create/2,User.set_password/3, and any response with a status in the new:retry_on_status. A test counting requests to the server may see more of them.max_retries: 0andretry_on_status: []restore the old behaviour. TypeDB.GivenandTypeDB.Duration.to_iso8601/1raise%TypeDB.Error{}with kind:encodewhere they raised:config. Code matching onkind: :configto catch an unencodable value must match:encode.- Five documented error codes were wrong and are corrected below. Code
matching on
TSV2,TSV3,TSV11orSRV5should re-read that entry — the driver was reporting what its test stub had invented, not what TypeDB answers.
Everything else is additive.
Added
:retry_max_delay— a ceiling on any single backoff, whichever form produced it, including a caller's own:retry_backofffunction. Defaults to5_000;:infinityopts out.:deadline— a wall-clock budget for a whole call, retries and the waits between them included. Defaults to:infinity. Each attempt is given whichever is smaller, its own:timeoutor what the budget has left, and a retry that could not finish inside the budget is not started. Available per connection and on every function that already took:timeout.:retry_on_status— statuses to retry in addition to transport failures and timeouts. Defaults to[429, 502, 503, 504];[]opts out. A numericretry-afteris honoured, bounded by:retry_max_delay.:log_level— the quietest level a connection will log at,:noneto silence it. Every driver log line now goes through one place, and theTypeDBmoduledoc lists all of them.[:typedb, :operation, …]— a span per call into the public API, with every retry and token renewal inside it, reporting:attemptsand a low-cardinality:routesafe to use as a metric tag.[:typedb, :transaction, …]— a span perTypeDB.transaction/5, from open to commit, with an:outcomeof:commit,:rollback,:closeor:commit_failed.[:typedb, :retry, :exhausted]— emitted when a call stops retrying, with:attempts. The event to alert on.TypeDB.Telemetry.attach_default_logger/1anddetach_default_logger/0— a line per operation, transaction, sign-in and give-up, off unless asked for.:database,:transaction_typeand:transaction_idin telemetry metadata, including for/v1/query, which carries its database in the request body.TypeDB.Error.retryable?/1andTypeDB.Error.retryable_statuses/0— whether retrying could plausibly help, for the layer above the driver: retrying a whole transaction or requeueing a job, where the unit of work is bigger than one HTTP call. Callers were otherwise copyingkind in [:transport, :timeout]out of the driver's internals.TypeDB.ConceptRow.to_struct/2— builds a struct from a row, raising on a variable that names no field.Kernel.struct/2silently returns the struct's defaults there, which theto_map/1docs previously warned about at length instead of solving.- An API snapshot test.
test/api_snapshot.txtrecords the whole published surface and the suite fails when the code and the file disagree, so a SemVer decision is forced at the moment the API changes rather than at release. - A versioning policy in CONTRIBUTING: what the version number covers — telemetry event names and metadata keys, error kinds, the option set and its defaults, the transport behaviour — and what it does not.
Changed
- The default backoff is jittered.
{:exponential, base}now draws uniformly from0..base * 2 ** (n - 1)instead of returning that value exactly, so callers that failed together no longer retry together. Pass a function to:retry_backofffor a delay you can predict. - Retry eligibility is decided per operation, not per HTTP method. Read
queries, opening a
:readtransaction,analyze,rollback,close,Database.create/2andUser.set_password/3are now retried; writes, schema changes,commit,User.create/3and opening a:writeor:schematransaction are not. - Retries exhausted and token renewals that fail now log at
:warning. Both were silent. TypeDB.GivenandTypeDB.Duration.to_iso8601/1raise%TypeDB.Error{}with the new kind:encoderather than:config.:configmeans the driver was configured wrongly at start-up; these mean an Elixir term has no TypeDB wire value.Error.kind()gains:encode.- CI now compiles and runs the unit suite on Windows through all three HTTP
adapters, so the claim that the driver is pure Elixir is proven rather than
assumed.
mix typedb.checkstill wants a POSIX shell there. - A
decimalattribute is now stripped of TypeQL'sdecsuffix whether or not the optionalDecimallibrary is loaded. Without it the fallback used to hand back"12.345dec"where theDecimalpath gave12.345, so the value differed in content, not just in type, depending on which dependencies happened to be installed. Found by the new optional-dependency CI job. - The supported TypeDB range is now stated as 3.12 or newer, where the
README said "3.x". Measured against 3.5.0, the driver does not work at all
there:
givenrows are rejected,/v1/serversdoes not exist, several error codes differ and insert-then-match fails. CI runs the integration suite against3.12.1andlatest. The exact floor between 3.5 and 3.12 is not established (tdb-vtg.6). - TypeDB.Transport and TypeDB.Token are internal and no longer published in the documentation. They were never meant to be called directly.
- Five error codes were wrong. Verified against a live TypeDB 3.12.1 and
corrected in the stub, the unit tests and the documentation: opening a
transaction on an unknown database answers
400 SRV3(not404 TSV2); committing a read transaction answers400 TSV2(not400 TSV3); any operation on a finished transaction answers404 TSV12(not404 TSV11);/v1/databases/{name}/schemaon an unknown database answers404 SRV3(not404 SRV5); and a one-shot query on an unknown database answers400 SRV3. Code matching on any of the old values must change. TypeDB.transaction/5no longer rolls back a failed:readblock. TypeDB rejects that with400 TSV3, so it was a wasted round trip; the transaction is closed instead, and its telemetry:outcomeis:close.TypeDB.Transaction.open/4raisesArgumentErrornaming the bad transaction type and the three accepted ones, where it raisedFunctionClauseError.Exception.message/1on a%TypeDB.Error{}now includes the HTTP status:[server 404] TSV2: Database not found.The rendered form is what reaches a log line and an exit reason, where nobody has the struct to inspect. Message text remains outside SemVer — match on:kindand:code.
0.1.0 - 2026-07-31
Initial release. Complete coverage of the TypeDB HTTP API v1, verified against TypeDB 3.12.1 on Elixir 1.20 / OTP 29.
Added
TypeDB— connection supervision, one-shot queries and bracketed transactions.TypeDB.Connection— lazy sign-in, transparent token renewal bounded by:max_auth_renewals, and per-connection configuration held in a read-concurrent ETS table so requests run in the caller's process.TypeDB.Database— list, get, create, create-if-not-exists, delete, schema and type-schema.exists?/2raises rather than answeringfalsewhen it could not reach the server, sincefalseis the answer that makes a caller create something that already exists.TypeDB.User— list, get, create, set password, delete.TypeDB.Server— health, version and cluster membership.TypeDB.Transaction— explicit:read,:writeand:schematransactions withquery/3,analyze/3,commit/2,rollback/2and idempotentclose/2, each taking its own:timeout.TypeDB.Answer—Ok,ConceptRowsandConceptDocuments; the latter two areEnumerable.TypeDB.ConceptRow—Access-backed rows, plusvalue/2,typed_value/2andto_map/1.TypeDB.Concept— structs for entities, relations, attributes, values and every type kind, with conversion of TypeDB values to native Elixir terms.TypeDB.DurationandTypeDB.DateTimeTZ— lossless representations of TypeDB'sdurationanddatetime-tzvalues, keeping the original wire string so TypeDB's nanosecond precision survives conversion to Elixir's coarser types.DateTimeTZ.new/2builds one for writing, from aNaiveDateTimeplus an IANA zone name or a UTC offset.TypeDB.Options— transaction and query options.TypeDB.Given— encodes input rows for TypeQL'sgivenstage into TypeDB's tagged wire form, making parameterised queries safe against TypeQL injection for arbitrary input. The API's raw-JSON form is not: TypeDB parses a bare string as a TypeQL literal, so a value containing a quote is a parse error.TypeDB.Error— a single exception type carrying TypeDB's stable error codes. Every function that can fail has both a{:ok, _} | {:error, %TypeDB.Error{}}form and a!form that raises, exceptTypeDB.transaction/5, which returns the block's own value.TypeDB.HTTP— a transport behaviour with three adapters:TypeDB.HTTP.Finch(the default, a Finch pool per connection),TypeDB.HTTP.Reqfor applications already running Finch through Req, andTypeDB.HTTP.Httpcfor deployments that must run on OTP alone. All three verify TLS by default and are covered by the same test suite.- TypeDB.Transport — request building, retries and response decoding, split out of the connection process.
- TypeDB.Token — reads a token's lifetime from its JWT claims so the driver can
renew before expiry instead of discovering it from a
401. TypeDB.Telemetry—[:typedb, :request, …]and[:typedb, :sign_in, …]spans. Logging is deliberately sparse and carries:typedb_connectionin its Logger metadata; see the "Logging" section ofTypeDB.TypeDB.JSON— a codec behaviour resolving to the built-inJSON, toJason, or to a codec you configure.mix typedb.check— validates.tqlfiles with TypeDB'stypeql-checkCLI.
Verified under load
- 200-way concurrent bursts, concurrent writes and long transactions straddling
token expiry, against servers configured with one- and five-second token
lifetimes: no failures, no lost writes. Renewals coalesce into a single sign-in
per generation, and
:max_auth_renewalsbounds how many times one request will renew before giving up. - Transport throughput measured against a local TypeDB 3.12.1, 400 requests per
run: Finch sustains ~1900 req/s at 200-way concurrency where
:httpcmanages 77 with multi-second tail latency. Finch is the default for that reason.
Verified against
- TypeDB 3.12.1 (HTTP API v1) on Elixir 1.20.2 / OTP 29, including an opt-in
suite that checks the TLS defaults against a server started with
--server.encryption.enabled.