Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ ADR justifying it.
| **v0.2 (APIs)** | Document, Schema, Branch, Streaming, Document.query/stream, guides, Livebook | **Done** |
| **v0.3 (versioned workflows)** | Commit, Diff, Merge (rebase), WOQL DSL v0.1, TerminusDB 12 compat, integration tests | **Done** |
| **v0.3.1 (WOQL v0.2)** | ~70 WOQL operators (Tier 1+2), 4-wrapper value model, dual path DSL, ADR-0008 | **Done** |
| v0.3.2+ | GraphQL, temporal/Allen WOQL, RDF list library, CSV/IO, benchmarks, tutorials | Pending |
| v0.3.2+ | GraphQL, temporal/Allen WOQL, RDF list library, CSV/IO, range queries, prefix mgmt, squash/reset/optimize, patch/apply, triples, remote (clone/fetch/push/pull), WOQL streaming, benchmarks, tutorials, ADR-0009..0013 | **Done** |
| v0.4 (Ecto) | `use TerminusDB.Schema` macro, schema generation, optional `:ecto` dep; research full `Ecto.Adapter` feasibility | Pending |
| v0.5 (ExDatalog) | `TerminusDB.Datalog` bridge (`to_woql`, `to_jsonld`, `from_triples`), optional `:datalog` dep | Pending |
| Release | CHANGELOG, release notes, migration guide, Hex.pm review, final review | Pending |
Expand Down
115 changes: 115 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,121 @@ All notable changes to `terminusdb_ex` are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.2] — 2026-06-26

### Added — GraphQL (ADR-0009)

- `TerminusDB.GraphQL` module: `query/3`, `mutate/3`, `introspect/2` — thin
HTTP wrapper for the `/api/graphql/{org}/{db}` endpoint.

### Added — Temporal / Allen WOQL (ADR-0010)

- `interval/3`, `interval_start_duration/3`, `interval_duration_end/3`,
`interval_relation/5`, `interval_relation_typed/3`, `date_duration/3`,
`day_after/2`, `day_before/2`, `weekday/2`, `weekday_sunday_start/2`,
`iso_week/3`, `month_start_date/2`, `month_end_date/2`,
`month_start_dates/3`, `month_end_dates/3`, `in_range/3`, `sequence/5`,
`range_min/2`, `range_max/2`.

### Added — RDF list library (ADR-0011)

- `TerminusDB.WOQL.RDFList` module with 17 functions: `rdflist_list/2`,
`rdflist_peek/2`, `rdflist_last/2`, `rdflist_nth0/3`, `rdflist_nth1/3`,
`rdflist_member/2`, `rdflist_length/2`, `rdflist_pop/2`, `rdflist_push/3`,
`rdflist_append/3`, `rdflist_clear/2`, `rdflist_empty/1`,
`rdflist_is_empty/1`, `rdflist_slice/4`, `rdflist_insert/4`,
`rdflist_drop/2`, `rdflist_swap/3`.

### Added — CSV / IO (ADR-0012)

- `WOQL.get/2`, `WOQL.put/3`, `WOQL.woql_as/1`, `WOQL.file/2`,
`WOQL.remote/2`, `WOQL.post/2`.

### Added — Range queries

- `triple_slice/5`, `quad_slice/6`, `triple_slice_rev/5`,
`quad_slice_rev/6`, `triple_next/4`, `quad_next/5`,
`triple_previous/4`, `quad_previous/5`.

### Added — Client API gaps

- `TerminusDB.Prefix` module: `get/2`, `add/3`, `update/3`, `upsert/3`,
`delete/2`, `all/2`.
- `Branch.squash/2`, `Branch.reset/3`, `Database.optimize/2`.
- `TerminusDB.Patch` struct: `from_json/1`, `to_json/1`, `update/1`,
`before/1`, `copy/1`.
- `Diff.diff_object/2`, `Diff.diff_version/2`, `Diff.patch/2`,
`Diff.patch_resource/2`, `Diff.apply/3`.
- `Commit.document_history/3`.
- `TerminusDB.Triples` module: `get/2`, `update/3`, `insert/3`.
- `WOQL.execute_stream/3` — streaming WOQL results.
- `TerminusDB.Remote` module: `clone/4`, `fetch/2`, `push/3`, `pull/3`.

### Added — Benchmarks

- `benchee` dev dependency.
- `TerminusDB.Benchmark` helper module.
- 5 benchmark suites in `bench/`.

### Added — Tutorials

- `guides/graphql-guide.md` — GraphQL queries, mutations, filters, pagination.
- `guides/temporal-allen-guide.md` — Intervals, Allen relations, calendar ops.
- `guides/csv-import-guide.md` — CSV reading/writing with WOQL.
- `guides/rdf-list-guide.md` — RDF list manipulation.

### Changed

- `rdflist_push/2` changed to `rdflist_push/3`: now takes a `new_head_var`
parameter so callers can update their reference to the new list head.
- `Benchmark.seed_database/2` return type changed from `Config.t()` to
`{Config.t(), String.t()}` so callers can clean up the database after
benchmarking.
- `Patch.before/1` and `Patch.update/1` asymmetry documented: `before/1`
preserves non-SwapValue fields for full state reconstruction; `update/1`
only includes changed (SwapValue) fields.
- `WOQL.execute_stream/3` now uses lazy `Stream.map` instead of eager
`Enum.map`, and safe `Jason.decode/1` instead of `Jason.decode!/1`.
- `GraphQL.query/3`, `GraphQL.mutate/3`, `GraphQL.introspect/2`, and all
`Prefix.*` functions now return `{:error, %Error{reason: :config}}` when
no database is scoped, instead of raising.

### Fixed — Review fixes (review-0.3.2.md)

- `rdflist_nth0`/`rdflist_nth1` variable-index path: fixed missing `v.dec`
variable in localize map; added `eval(minus(...))` for decrement; unrolled
recursion to avoid infinite Elixir recursion (C1).
- `rdflist_slice/4`: fixed silently ignored `end_val` parameter; rewrote
with proper start/end bounds using cell navigation + collect (C2).
- `rdflist_swap/3`: fixed no-op implementation; added `rdflist_cell_at`
helper and delete_triple/add_triple write operations for both cells (C3).
- `rdflist_drop/2`: fixed to operate on the cell at `position`, not the list
head; position 0 deletes from head, position > 0 navigates to cell and
relinks parent (H1).
- `rdflist_clear/2`: wrapped delete_triple calls inside `opt(and_([triple,
delete]))` blocks to guard against unbound variables (M4).
- `encode_arithmetic/1`: added clause for non-variable binary strings (L2).
- `Benchmark.seed_database/2`: returns `{config, db_name}` for cleanup (L4).
- RDFList tests: added structural AST assertions verifying write operations
(AddTriple/DeleteTriple) in swap/drop, dec variable in nth, Optional
blocks in clear (L5).
- `encode_value/1`: added list encoding for `range_min`/`range_max`
operands (uses `DataValue` wrapper with `list` field).

### Deferred to v0.3.3+

- GraphQL builder DSL.
- `graph/1` context setter.
- Data version headers (`last_data_version`/`get_data_version`).
- Gzip compression for large document inserts.
- Macro sugar layer (`TerminusDB.WOQL.Macros`).

### Deferred to v0.4+

- Access control (organizations, users, roles, capabilities).
- DataFrame (Explorer) integration.
- Ecto integration (`TerminusDB.Schema` macro).

## [0.3.1] — 2026-06-25

### Added — WOQL DSL v0.2 (ADR-0008)
Expand Down
58 changes: 52 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ graph database with built-in version control. It is built on
[Req](https://hexdocs.pm/req) and treats connection context as **immutable data**,
making it safe for concurrent use.

> **Status:** v0.3.1. `Config`, `Error`, `Telemetry`, `Client`, `Database`,
> **Status:** v0.3.2. `Config`, `Error`, `Telemetry`, `Client`, `Database`,
> `Document` (with streaming), `Schema`, `Branch`, `Commit`, `Diff`, `Merge`,
> and `WOQL` (v0.2 DSL — ~70 operators) are implemented and tested. GraphQL,
> Ecto, and ExDatalog integrations are planned for later milestones. See
> `ARCHITECTURE.md` and `AGENTS.md` for the roadmap.
> `Patch`, `Prefix`, `Triples`, `Remote`, `GraphQL`, and `WOQL` (v0.2 DSL —
> ~100 operators including temporal/Allen, CSV/IO, range queries, RDF list
> library) are implemented and tested. Ecto and ExDatalog integrations are
> planned for later milestones. See `ARCHITECTURE.md` and `AGENTS.md` for
> the roadmap.

## Installation

Expand All @@ -24,7 +26,7 @@ Add `terminusdb_client` to your dependencies in `mix.exs`:
```elixir
def deps do
[
{:terminusdb_client, "~> 0.3.1"}
{:terminusdb_client, "~> 0.3.2"}
]
end
```
Expand Down Expand Up @@ -90,6 +92,49 @@ TerminusDB.Document.stream(config, type: "Person")
{:ok, _} = TerminusDB.Database.delete(config, "mydb")
```

## GraphQL

TerminusDB auto-generates a GraphQL endpoint from your document schema.
Query and mutate using raw GraphQL strings:

```elixir
# Query all Person documents
{:ok, result} = TerminusDB.GraphQL.query(config, "{ Person { name age } }")
# => {:ok, %{data: %{"Person" => [%{"name" => "Alice", "age" => 30}]}, errors: nil}}

# Insert via mutation
{:ok, _} = TerminusDB.GraphQL.mutate(config, "mutation { _insertDocuments(json: \"...\") }")

# Introspect the auto-generated schema
{:ok, schema} = TerminusDB.GraphQL.introspect(config)
```

## WOQL queries

The WOQL DSL provides ~100 composable operators for graph queries:

```elixir
import TerminusDB.WOQL

# Query with select, triple pattern, and limit
{:ok, result} =
WOQL.execute(config,
select(["v:Name", "v:Age"],
limit(10,
and_([
triple("v:Person", "name", "v:Name"),
triple("v:Person", "age", "v:Age"),
triple("v:Person", "rdf:type", iri("@schema:Person"))
])
)
)
)

# Stream WOQL results (lazy, constant memory)
{:ok, stream} = WOQL.execute_stream(config, query)
Enum.to_list(stream)
```

All public functions return `{:ok, result}` or `{:error, %TerminusDB.Error{}}`. Each
`!/1`-suffixed variant raises `TerminusDB.Error` instead.

Expand All @@ -104,7 +149,8 @@ config = TerminusDB.Config.new(endpoint: "http://localhost:6363", token: "tok_ab
## Telemetry

Every operation emits `[:terminusdb, <area>, :start]` and `[:stop]` events
(`<area>` is `:database`, `:document`, `:query`, `:branch`, `:merge`, `:diff`, or
(`<area>` is `:database`, `:document`, `:query`, `:branch`, `:merge`, `:diff`,
`:commit`, `:woql`, `:graphql`, `:prefix`, `:triples`, `:remote`, or
`:connection`). Attach with `:telemetry.attach_many/4`. See `TerminusDB.Telemetry`
and ADR-0005.

Expand Down
36 changes: 36 additions & 0 deletions bench/document_crud_bench.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Document CRUD Benchmark
#
# Run: mix run bench/document_crud_bench.exs
# Requires: docker compose up -d

alias TerminusDB.{Benchmark, Config, Document}

endpoint = System.get_env("TERMINUSDB_URL", "http://localhost:6363")

config =
Config.new(endpoint: endpoint, user: "admin", key: "root")
|> Config.with_database("bench_crud_#{:erlang.unique_integer([:positive])}")

# Seed
{seeded, db_name} = Benchmark.seed_database(config, 100)

Benchee.run(
%{
"insert 1 doc" => fn ->
Document.insert(seeded, %{"@type" => "Person", "name" => "Bench_#{:erlang.unique_integer([:positive])}", "age" => 25},
author: "admin", message: "bench insert"
)
end,
"get by id" => fn ->
Document.get(seeded, id: "Person/Person_1")
end,
"get by type" => fn ->
Document.get(seeded, type: "Person", as_list: true, count: 10)
end
},
time: 10,
memory_time: 2
)

# Cleanup
TerminusDB.Database.delete(config, db_name, force: true)
21 changes: 21 additions & 0 deletions bench/path_parse_bench.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Path Parse Benchmark
#
# Run: mix run bench/path_parse_bench.exs

alias TerminusDB.WOQL.Path

Benchee.run(
%{
"simple pred" => fn -> Path.parse("friend") end,
"star" => fn -> Path.parse("friend*") end,
"plus" => fn -> Path.parse("friend+") end,
"inverse" => fn -> Path.parse("<friend") end,
"alternation" => fn -> Path.parse("friend|foe") end,
"sequence" => fn -> Path.parse("friend,foe") end,
"quantified" => fn -> Path.parse("friend*{1,3}") end,
"complex" => fn -> Path.parse("(friend,foe)+{2,5}") end,
"inverse star" => fn -> Path.parse("<friend*") end
},
time: 5,
memory_time: 2
)
33 changes: 33 additions & 0 deletions bench/streaming_bench.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Streaming vs Batch Benchmark
#
# Run: mix run bench/streaming_bench.exs
# Requires: docker compose up -d

alias TerminusDB.{Benchmark, Config, Document}

endpoint = System.get_env("TERMINUSDB_URL", "http://localhost:6363")

config =
Config.new(endpoint: endpoint, user: "admin", key: "root")
|> Config.with_database("bench_stream_#{:erlang.unique_integer([:positive])}")

# Seed with 500 documents
{seeded, db_name} = Benchmark.seed_database(config, 500)

Benchee.run(
%{
"get all (as_list)" => fn ->
Document.get(seeded, type: "Person", as_list: true)
end,
"stream (collect)" => fn ->
seeded
|> Document.stream(type: "Person")
|> Enum.to_list()
end
},
time: 10,
memory_time: 2
)

# Cleanup
TerminusDB.Database.delete(config, db_name, force: true)
32 changes: 32 additions & 0 deletions bench/woql_decode_bench.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# WOQL Decode Benchmark
#
# Run: mix run bench/woql_decode_bench.exs

alias TerminusDB.WOQL

# Pre-encode queries for decoding
triple_jsonld = WOQL.to_jsonld(WOQL.triple("v:S", "p", "v:O"))

and_jsonld = WOQL.to_jsonld(WOQL.and_([
WOQL.triple("v:S", "name", "v:Name"),
WOQL.triple("v:S", "age", "v:Age"),
WOQL.triple("v:S", "rdf:type", WOQL.iri("@schema:Person"))
]))

select_jsonld = WOQL.to_jsonld(WOQL.select(["v:Name"], WOQL.limit(10, WOQL.triple("v:P", "name", "v:Name"))))

path_jsonld = WOQL.to_jsonld(WOQL.path("v:S", "friend*{1,3}", "v:O"))

interval_jsonld = WOQL.to_jsonld(WOQL.interval_relation("v:R", "v:XS", "v:XE", "v:YS", "v:YE"))

Benchee.run(
%{
"triple" => fn -> WOQL.from_jsonld(triple_jsonld) end,
"and_ (3 ops)" => fn -> WOQL.from_jsonld(and_jsonld) end,
"select + limit" => fn -> WOQL.from_jsonld(select_jsonld) end,
"path" => fn -> WOQL.from_jsonld(path_jsonld) end,
"interval_relation" => fn -> WOQL.from_jsonld(interval_jsonld) end
},
time: 5,
memory_time: 2
)
41 changes: 41 additions & 0 deletions bench/woql_encode_bench.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# WOQL Encode Benchmark
#
# Run: mix run bench/woql_encode_bench.exs

alias TerminusDB.{WOQL, Benchmark}

Benchee.run(
%{
"triple" => fn -> WOQL.to_jsonld(WOQL.triple("v:S", "p", "v:O")) end,
"and_ (3 ops)" => fn ->
WOQL.to_jsonld(WOQL.and_([
WOQL.triple("v:S", "name", "v:Name"),
WOQL.triple("v:S", "age", "v:Age"),
WOQL.triple("v:S", "rdf:type", WOQL.iri("@schema:Person"))
]))
end,
"select + limit" => fn ->
WOQL.to_jsonld(WOQL.select(["v:Name"], WOQL.limit(10, WOQL.triple("v:P", "name", "v:Name"))))
end,
"order_by" => fn ->
WOQL.to_jsonld(WOQL.order_by([{"v:Age", :asc}], WOQL.and_([
WOQL.triple("v:P", "name", "v:Name"),
WOQL.triple("v:P", "age", "v:Age")
])))
end,
"path (string)" => fn ->
WOQL.to_jsonld(WOQL.path("v:S", "friend*{1,3}", "v:O"))
end,
"interval_relation" => fn ->
WOQL.to_jsonld(WOQL.interval_relation("v:R", "v:XS", "v:XE", "v:YS", "v:YE"))
end,
"triple_slice" => fn ->
WOQL.to_jsonld(WOQL.triple_slice("v:S", "v:P", "v:O", 10, 100))
end,
"get (CSV)" => fn ->
WOQL.to_jsonld(WOQL.get(WOQL.woql_as([{"name", "v:Name"}]), WOQL.file("data.csv")))
end
},
time: 5,
memory_time: 2
)
10 changes: 8 additions & 2 deletions coveralls.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{
"coverage_options": {
"minimum_coverage": 80
}
"minimum_coverage": 80,
"treat_no_relevant_lines_as_covered": true
},
"skip_files": [
"lib/terminus_db/benchmark.ex",
"lib/terminus_db/application.ex",
"test/support/helpers.ex"
]
}
Loading
Loading