All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[0.7.0] - 2026-07-14
Added
Arcadic.Server.database_info/1— database-level info (%{database, type, records, classes, size_bytes}) for the connection's database. Over gRPC it maps toGetDatabaseInfo(all fields); over HTTP toSELECT FROM schema:database(name/size; record and class counts arenil, not cheaply available on that transport).Arcadic.Transport.Grpc— an optional third transport over ArcadeDB's gRPC plugin, behind the new optional deps{:grpc, "~> 0.11"}and{:protobuf, "~> 0.17"}. Its headline is streaming:StreamQueryinCURSORmode is a real server cursor (O(n), server-paced, language-agnostic), where HTTP result streaming offset-pages (O(n²) in the general case) and Bolt streams Cypher only. The transport implements the full surface it can support:- Reads/writes —
execute/4(ExecuteQuery/ExecuteCommand) andquery_stream/4(the CURSOR win). - Transactions —
begin/commit/rollback, soArcadic.transaction/3and tx-scoped reads/writes work over gRPC. - Bulk graph ingest —
Arcadic.Bulk.ingest/3(viaGraphBatchLoad), the gRPC twin of the HTTP/batchendpoint (same counts/id_mappingresult; transport-transparent). Arcadic.Ingest— document bulk-insert into a class (BulkInsert, orInsertStreamvia:chunk_size), returning insert-summary counts.Arcadic.Record— single-record create/lookup/update/delete by@rid(raw maps).- Admin —
Arcadic.Serverdatabase management (list_databases/database_exists?/create_database/drop_database/info/database_info) andArcadic.explain/profile. Arcadic.Transport.Grpc.ChannelPool— an opt-in, caller-supervised shared-channel cache; add it to your supervision tree for channel reuse (absent, a fresh channel per call is used).
Errors are value-free (an atom reason, never the gRPC wire message; per-row ingest errors surface only a row index + a categorical code). Transport is plaintext by default; enabling TLS (a secure
grpcs://scheme ortransport_options: [tls: true]) verifies the server certificate against the OS trust store (verify_peer, neververify_none). A few operations are intentionally HTTP-only: server settings, user management (the server itself does not implement it over gRPC), token login/logout, time-series, and HA read-consistency — use an HTTPConnfor those. Select it withtransport: Arcadic.Transport.Grpcand agrpc://host:portURL; credentials come fromConn.auth. HTTP/Bolt-only consumers are unaffected: the transport and its generated protobuf stubs are compile-guarded on the optional deps, so a consumer that doesn't add:grpc/:protobufcompiles and ships without them.- Reads/writes —
[0.6.0] - 2026-07-14
Added
Arcadic.transaction/3accepts an opt-in:retryoption (truefor defaults,max_attempts: 3, base_backoff_ms: 50, max_backoff_ms: 1000, or a keyword overriding any of those). Off by default (unchanged behavior). On a transient server fault (:concurrent_modification,:not_leader, and a pre-commit:timeout) it retries with jittered exponential backoff up to:max_attempts. The retried function must be idempotent - it may run more than once. Emits[:arcadic, :transaction, :retry]telemetry per attempt (%{attempt}measurement,%{reason}metadata).Arcadic.Conn.with_consistency/2andconnect(consistency: ...): a read-consistency level for subsequent reads::eventual(default, sends no extra header),:read_your_writes, or:linearizable. HTTP-only; a non-default level on a Bolt connection raises. On a single-server deployment,:read_your_writesis a harmless no-op.Arcadic.query_bookmarked/4/command_bookmarked/4(+!variants), likequery/4/command/4but return{:ok, rows, conn'}, whereconn'carries a monotonically-advancing read-your-writes bookmark. Threadconn'into the next call to observe your own writes. HTTP-only; bookmarked calls target the primary host and do not participate in multi-host failover.connect(hosts: [...]): additional base URLs for multi-host availability failover. Reads fail over to the next host on any connection error; writes fail over only on a pre-send connect error, never on an ambiguous post-send close. A session (insidetransaction/3) pins to whichever host answers first. This is availability failover, not load balancing; front a cluster with a load balancer if you want request distribution.- New
Arcadic.Errorreason:not_leader: the target node is not the cluster leader and could not forward the write (ServerIsNotTheLeaderException). Treated as retriable by managed retry and as failover-eligible by multi-host failover (the write was rejected, nothing applied). Arcadic.Changes— a live change-events client for ArcadeDB's/wsfeed. Start it under your own supervision tree (start_link/1) andsubscribe/3a database to receive{:arcadic_change, %Arcadic.Changes.Event{}}messages. Best-effort at-most-once: the feed has no replay, so a reconnect delivers a:reconnectedmarker (events during the gap are lost) and a full delivery buffer delivers one:overflowmarker after dropping the oldest events — either marker means the subscriber should reconcile. A terminal401/403handshake failure delivers{:arcadic_change_error, :unauthorized}(then stops, no reconnect spin); a server-rejected subscribe delivers a non-terminal{:arcadic_change_error, :subscribe_rejected}. Requires the optionalmint_web_socketdependency;start_link/1returns{:error, :mint_web_socket_not_available}without it, and validates the:connvalue-free (:invalid_auth/:invalid_url_scheme/:invalid_max_buffer— an unrecognized URL scheme is refused, never downgraded to plaintext).Arcadic.Function—define/4/delete/2(+!) for ArcadeDB user-defined functions (DEFINE FUNCTION/DELETE FUNCTION). A function name is a dottedlibrary.fn; the body is a single-line, single-quoted literal (ArcadeDB's DDL string literal has no escape).define/4takes a single trailingoptskeyword list —:params(a list of parameter names) and:language(:jsdefault,:sql,:cypher) — matching the other DDL helpers. Call a defined function from an ordinaryquery/4/command/4via the backtick idiom.Arcadic.Trigger—create/4/drop/2(+!) for ArcadeDB triggers (CREATE TRIGGER/DROP TRIGGER), firing a:timing(:before/:after) ×:event(:create/:delete/:update/:read) action in:sql,:javascript, or:java. SharesArcadic.Function's single-line/single-quoted body limit.Arcadic.MaterializedView—create/3/drop/2(+!) for ArcadeDB materialized views (CREATE MATERIALIZED VIEW/DROP MATERIALIZED VIEW) from a rawSELECTstatement.Arcadic.Geo—create_index/4/drop_index/3(+!) for aGEOSPATIALindex over a string property holding WKT (this ArcadeDB build has no nativePOINTschema type). Geospatial querying rides ordinaryquery/4/command/4.Arcadic.TimeSeries— a client for ArcadeDB's time-series wire family. Requires ArcadeDB ≥ 26.7.2 (an older server 404s every/api/v1/tsroute).create_type/4/drop_type/2(+!) —TIMESERIESDDL (fields, tags, precision, shards, retention, compaction interval);add_downsampling/3/drop_downsampling/2(+!) — downsampling policies.create_aggregate/3/refresh_aggregate/2/drop_aggregate/2(+!) — continuous aggregates (CREATE/REFRESH/DROP CONTINUOUS AGGREGATE), materializing a rawSELECTas a document type.write/3/write_lines/3(+!) — Influx line-protocol writes:write/3builds the wire format from structured point maps (typed fields, string tags, Identifier-validated names);write_lines/3is a raw passthrough for an already-built line-protocol batch. Append-only, non-idempotent — see the module's Operational contract before relying on retries or mixed-type batches.query/3/latest/3(+!) — JSON reads:query/3returns the raw columnar shape or, with:aggregation+:bucket_interval, bucketed aggregations;latest/3returns the newest point matching at most one tag.prom_query/3,prom_query_range/6,prom_labels/2,prom_label_values/3,prom_series/3(+!) — the PromQL read family (instant, range, labels, label values, series matching), decoding the Prometheusdataenvelope.- Four new optional
Arcadic.Transportcallbacks (ts_write/3,ts_query/3,ts_latest/3,ts_prom_get/4) back the wire family, HTTP-only (Bolt returns{:error, %Arcadic.Error{reason: :not_supported}}). - New
notebooks/timeseries.livemd— DDL, writes, query/latest, PromQL, continuous aggregates, downsampling, and pointing Prometheus/Grafana at ArcadeDB.
Changed
connect/3andtransaction/3now reject unknown option keys value-free (anArgumentErrornaming the allowed set), matchingquery/command. A typo'd option —consistancy:, orretires:forretry:— was previously ignored silently, which for a connection-control option meant the wrong default applied (a mis-spelledretry:silently meant no retry). The offending value is never echoed.
[0.5.0] - 2026-07-10
Added
Arcadic.explain/4+Arcadic.profile/4(+!) — surface the ArcadeDB EXPLAIN/PROFILE plan (%{plan, plan_tree, rows}) thatquery/commandsilently dropped.explainis plan-only;profileexecutes the statement (a write mutates). Works over HTTP and Bolt (Cypher-only).query/command/query_streamnow return{:error, %Arcadic.Error{reason: :use_explain}}on a bare EXPLAIN/PROFILE (was a silent{:ok, []}).Arcadic.Server— server administration, expanded beyond database lifecycle:info/2(server info/metrics,mode: :basic | :default | :cluster),metrics/1,health?/1,events/1,set_server_setting/3/set_database_setting/3(key + value both allowlist-validated value-free),open_database/2/close_database/2,align_database/2(cluster-only — a single-server node returns a server error),check_database/2(fix: truerunsCHECK DATABASE FIX, returns the integrity map),profiler/2, andshutdown/1(a successful shutdown typically surfaces as a transport-closed error, since the server stops responding mid-request), alongside the existingcreate_database/2/drop_database/2/database_exists?/2/list_databases/1/ready?/1. HTTP-only, tenant-blind.Arcadic.Security— server security & auth admin (HTTP-only):login/1/logout/1(session tokens),sessions/1,users/1,groups/1,api_tokens/1,create_user/2, anddrop_user/2(all +!).create_user/2's password is JSON-encoded into the server command and never echoed — an unencodable spec (e.g. a non-UTF-8 password) is rejected value-free as{:error, :invalid_user_spec}before any wire call.Arcadic.Backup— backup and restore (HTTP-only):backup/2(BACKUP DATABASE, optional:totarget URL),list/1, andrestore/3(all +!). A:totarget and the restore URL are allowlist-validated viaArcadic.Identifier.validate_url/1before interpolation (neither command can bind a URL param), value-free on rejection ({:error, :invalid_url}).Arcadic.Conn.with_bearer/2— derive a Bearer-token-authenticated connection from a session token (typicallyArcadic.Security.login/1's); HTTP-only.Arcadic.Schema—stats/1(schema:stats, per-database operation counters),dictionary/1(schema:dictionary), andmaterialized_views/1(schema:materializedviews) (all +!),@props-stripped like the rest of the module.- Every
Arcadic.Server/Arcadic.Security/Arcadic.Backupcall emits a value-free[:arcadic, :admin, :start | :stop | :exception]telemetry span (metadata::operation,:reason— no database name, setting key/value, URL, or credential). Arcadic.command/4/command_async/4accept an:auto_commitboolean opt, forwarded as-is to ArcadeDB'sautoCommitbody param.Arcadic.FullText— full-text search:FULL_TEXT(Lucene) index DDL (create_index/4+drop_index/3, retro-indexes existing rows) andSEARCH_INDEX/SEARCH_FIELDSquery builders (search/5,search_fields/5,:with_scoreprojects the BM25$score). HTTP-only SQL.Arcadic.Bulk—ingest/3(+!): bulk-creates vertices and edges over ArcadeDB'sPOST /api/v1/batch/<db>NDJSON endpoint in one atomic POST (edges wire via a structural"@id"temp key on vertex records; the response'sid_mappingmaps each temp"@id"to its assigned real RID). Create-only, HTTP-only, via a new optionalbatch_ingest/3transport callback.Arcadic.Param—int8/1/bytes/1: typed param-value wrappers ($int8/$bytes) decoded server-side to abyte[]before the query runs. HTTP-only, requires ArcadeDB ≥ 26.5.1.Arcadic.Vector.fuse/3now accepts heterogeneous neighbor specs — dense (bare 4-tuple),:sparse, and:fulltext— fused together in one hybrid-ranked result set.- New notebook
notebooks/graphrag.livemd— an end-to-end graphRAG walkthrough (bulk ingest, idempotentUNWINDupsert, dense/sparse/full-text/hybrid retrieval, INT8 params, traversal).
Fixed
- Docs: SQL
:namevs Cypher$nameparameter binding is now documented; the Bolt-streaming example usesArcadic.Transport.Bolt.setup/1(the priortransport_options: [bolt: …]form returned:not_supportedfor streaming);~> 0.5install pins; hexdoc module groups.
Notes
- Re-verified boltx temporal decode against ArcadeDB 26.8.1 (
:integration_boltgreen).
[0.4.0] - 2026-07-07
Added
Arcadic.Schema— tenant-blind schema introspection:types/1,properties/2,indexes/2(with a:typefilter),buckets/1, anddatabase/1(the engine config,schema:database) (all +!). SQL-onlySELECT FROM schema:*; a caller type name binds as a$paramand isIdentifier-shape-guarded (value-free); ArcadeDB's@propsserializer noise is deep-stripped at every nesting depth.Arcadic.Import—database/3(+!) wrappingIMPORT DATABASE. The source URL is validated against a positive character allowlist (closing the interpolated-URL injection surface, since the URL cannot be a bound parameter and ArcadeDB honours backslash-escapes inside string literals) and a scheme allowlist (http/https/file);with:accepts number, boolean, and charset-allowlisted string settings, emitted as ArcadeDB's no-parensWITH k = vgrammar. Import errors are reflected faithfully — a private/loopback host trips ArcadeDB's SSRF guard (:unauthorized/java.lang.SecurityException, distinct from an auth failure'sServerSecurityExceptionviaerror.exception).Arcadic.Export—database/3(+!) wrappingEXPORT DATABASE file://<name>, symmetric toArcadic.Import: the bare export name is path-traversal-guarded (value-free), andwith:reuses the same number/boolean/string settings grammar.- HTTP
query_streampages WHERE-less SQL by an O(n)@ridkeyset cursor (offset fallback for WHERE'd statements) and supports Cypher streaming via a callerorder_key: "id(v)"(offset,$nameplaceholders). The comment guard is language-aware (//rejected for Cypher). Insidetransaction/3over Bolt, streaming uses the O(n) in-transaction cursor. - Transaction-scoped Bolt streaming —
query_stream/4insidetransaction/3streams over the transaction's own connection (sees uncommitted writes), guarded so anexecutecannot interleave an open cursor on the shared socket; the cursor callbacks disconnect on a wire fault (desync-safe). Consume the stream inside thetransaction/3body (it is bound to the tx connection). - Bolt over TLS —
scheme: "bolt+s"withssl_opts.bolt+sis secure by default (verify_peeragainst the OS trust store, via boltx's invertedbolt+sscscheme under the hood);ssl_opts: [verify: :verify_none]is an explicit caller opt-in to skip verification. A:uriopt is rejected (it would bypass the scheme translation and silently skip verification). - Bolt now fails loud if a
BOLT_USER/BOLT_PWD/BOLT_HOST/BOLT_TCP_PORTenvironment variable is set — both at pool setup (start_link/1/setup/1) and again on every connect/reconnect, because boltx re-reads them with precedence over arcadic's explicit config at connect time (so a var set after startup is caught too); unset the var.
Changed
- Bolt RUN/PULL wire-framing is deduplicated to a single site (
stream_run/stream_pull), shared by the non-transaction stream and the in-transaction cursor callbacks.
Fixed
Arcadic.query/4,command/4, andquery_stream/4now reject a non-keyword-listoptswith a value-freeArgumentError("opts must be a keyword list"). Previously an improper-listopts(e.g.[:foo]) raised aKeyworderror whose message echoed the offending entry — a Rule-3 value leak on the core query paths. The opt-key guard is now shared across the query,Schema,Import, andVectorsurfaces.
Notes
- Documented an upstream ArcadeDB server hazard for operators running Bolt over TLS: the
listener performed TLS handshakes on its single shared accept thread, so one early-closed,
stalled, or untrusted-cert handshake could wedge Bolt for every client (~100% CPU tight loop or
no ServerHello) until restart. arcadic's client-side TLS is unaffected. Reported by arcadic;
root-caused and fixed upstream on
main2026-07-08 (per-connection handshake threads + read timeout), shipping in 26.7.2 — ArcadeData/arcadedb#5106. Builds predating the fix remain affected; the wedge is condition-dependent (a cleanly-deliveredunknown_caalert does not trigger it — early-close/stall do), so a clean probe on a pre-fix build proves nothing.
[0.3.0] - 2026-07-05
Added
Arcadic.Vectorsparse + hybrid completion:create_sparse_index/5(+!),drop_sparse_index/4(+!),sparse_neighbors/8(+!) over ArcadeDBLSM_SPARSE_VECTORindexes ((tokens, weights)pair; rows ranked by top-levelscore).create_sparse_indexopts:dimensions,modifier(:none|:idf).filter(param-bound candidate RID set),group_by, andgroup_sizeopts onneighbors/6,sparse_neighbors/8, andfuse/3.- A
[:arcadic, :vector, :sparse_index_preexisting]telemetry event when a sparse index is created over rows that already exist (which a sparse index does not retro-index).
[0.2.1] - 2026-07-05
Fixed
- Install instructions in the README and getting-started notebook pointed at a pre-publish
path dependency and
~> 0.1; corrected to{:arcadic, "~> 0.2"}from Hex.
Added
- README: Hex.pm + hexdocs badges, a Benchmarks section (linking the
bench/harness and the 100k result set), and a Bulk-loading note;usage-rules.mdbulk-loading entry (IMPORT DATABASE/transaction/3).
No library code changed in this release — docs/packaging only.
[0.2.0] - 2026-07-05
Added
Arcadic.Vector— dense vector search over ArcadeDBLSM_VECTORindexes:create_dense_index/5(+!),drop_dense_index/3(+!),neighbors/6(+!),fuse/3(+!), andindex_ref/2. Tenant-blind; the query vector,k,ef_search, andmax_distancebind as params; index refs are identifier-validated; metadata keys/values and query/fusion option inputs are allowlisted and validated value-free (similarity,encoding,quantization,fusionagainst their ArcadeDB enums).neighbors/6rows carry adistancewhose scale depends on the indexsimilarity(COSINE0..1ascending; DOT_PRODUCT negative);fuse/3rows are ranked byscore. Sparse retrieval and the Ash-native surface are named non-goals.
[0.1.0] - 2026-07-04
Added
Arcadic.Conn— pure-data connection handle (redactingInspect) andArcadic.connect/3/Arcadic.with_database/2to build and derive handles.Arcadic.query/4+query!/4(idempotent read endpoint) andArcadic.command/4+command!/4(write endpoint), params-only ($name), Cypher-default with SQL/Gremlin/GraphQL/Mongo/SQLScript opt-in.Arcadic.command_async/4— fire-and-forget write (server enqueues, returns:okon HTTP 202).- Session transactions:
Arcadic.transaction/3(commit on return, reraise on exception) andArcadic.rollback/2(intentional abort →{:error, reason}). Arcadic.Server— server admin:create_database/2(+!),drop_database/2(+!),database_exists?/2,list_databases/1,ready?/1.Arcadic.Error/Arcadic.TransportError— typed error taxonomy with boundary redaction (quarantineddetail, value-free reasons).Arcadic.Transport— the transport behaviour seam, with the defaultArcadic.Transport.HTTP(Req/Finch) implementation.Arcadic.Telemetry— value-free:telemetry.span/3spans (no statement, params, values, or database name).Arcadic.Identifier— allowlist identifier validation.- Migration runner:
Arcadic.Migration(behaviour),Arcadic.MigrationRegistry(use+migrations [...]), andArcadic.Migrator(migrate/2,status/2,rollback/3,reset/2,pending_migrations/2), tracking applied versions in_arcadic_migrations. Arcadic.Transport.Bolt— optional Bolt transport (Bolt v4, non-TLS scheme) via the optionalboltxdependency; server admin remains HTTP-only.Arcadic.query_stream/4— Bolt-only lazyStream.t()of raw row maps, chunked over BoltPULL/has_more(defaultchunk_size: 1000); a:timeoutopt bounds each RUN/PULL receive (default:infinity), raising%Arcadic.TransportError{reason: :timeout}on breach; guarded off HTTP and inside transactions with a typed:not_supported.Arcadic.Transport.Bolt.setup/1— single-sourcetransport_optionsbuilder ([bolt: pool, bolt_opts: opts]).Arcadic.Telemetry.event/3— allowlist-validated manual telemetry for lazy ops;[:arcadic, :query_stream, :start | :stop]events (value-free).
Fixed
Arcadic.Transport.Boltnow threadsconn.databaseinto every Bolt RUN/BEGIN, sowith_database/2selects the database on Bolt (was hitting the connection default).- Bolt
transaction/3maps a commit-failure to a typed%Arcadic.Error{reason: :transaction_error}instead of leaking DBConnection's bare:rollbackatom. Arcadic.Transport.Bolt— a failed Bolt connect (wrong password, or a Bolt conn pointed at a non-Bolt port) no longer leaks a:gen_tcpsocket. arcadic now owns the connect handshake and HELLO on both the per-stream connection and the DBConnection pool, closing the socket on every failure; a bad-password stream connect surfaces:unauthorized, and the connect HELLO is bounded byconnect_timeout. Connect-time errors are redacted on both sites: a HELLO response arcadic's parser cannot classify returns a value-free:bolt_protocol_errorinstead of a raw exception carrying server bytes, and the DBConnection pool's connect error drops the server-supplied failure message (keeping the error code/class) so it cannot ride a connect-failure log line.