defmodule TerminusDB.WOQL do
@moduledoc """
A functional builder DSL for WOQL (Web Object Query Language).
WOQL is TerminusDB's Datalog-based query language. This module provides a
comprehensive set of ~100 composable functions that build a `TerminusDB.WOQL.Query`
struct, which can be serialized to the JSON-LD wire format via `to_jsonld/1`
and executed via `TerminusDB.WOQL.execute/3`.
This is WOQL DSL v0.2 (ADR-0008) extended in v0.3.2 with temporal/Allen,
CSV/IO, range queries, and an RDF list library.
## Design
The DSL is purely functional (no macros). Each function returns a
`%WOQL.Query{}` struct and composes by nesting, mirroring the recommended
functional WOQL style. Variables are plain strings using the `v:Name`
convention.
The JSON-LD encoder uses four value-wrapper types matching the Python/JS
clients: `NodeValue` (nodes/IRIs), `Value` (generic values), `DataValue`
(literal data), and `ArithmeticValue` (arithmetic operands). Use `iri/1` to
explicitly mark a string as an IRI in triple object position (string objects
default to `xsd:string` literals).
## Supported vocabulary (v0.2)
### Logical combinators
| Function | WOQL JSON-LD type |
| --- | --- |
| `and_/1` | `And` |
| `or_/1` | `Or` |
| `not_/1` | `Not` |
| `opt/1` (alias `optional/1`) | `Optional` |
| `once/1` | `Once` |
| `immediately/1` | `Immediately` |
### Query modifiers
| Function | WOQL JSON-LD type |
| --- | --- |
| `select/2` | `Select` |
| `distinct/2` | `Distinct` |
| `limit/2` | `Limit` |
| `start/2` | `Start` |
| `order_by/2` | `OrderBy` |
| `group_by/4` | `GroupBy` |
| `count/2` | `Count` |
| `collect/3` | `Collect` |
| `star/0`, `all/0` | `Triple` (shortcut) |
### Graph patterns
| Function | WOQL JSON-LD type |
| --- | --- |
| `triple/3` | `Triple` |
| `quad/4` | `Triple` + `graph` |
| `added_triple/3`, `added_quad/4` | `AddedTriple` |
| `removed_triple/3`, `removed_quad/4` | `DeletedTriple` |
| `add_triple/3`, `add_quad/4` | `AddTriple` |
| `delete_triple/3`, `delete_quad/4` | `DeleteTriple` |
| `update_triple/3`, `update_quad/4` | `And` (macro) |
### Comparison
| Function | WOQL JSON-LD type |
| --- | --- |
| `eq/2` | `Equals` |
| `less/2` | `Less` |
| `greater/2` | `Greater` |
| `gte/2` | `Gte` |
| `lte/2` | `Lte` |
| `like/3` | `Like` |
### Schema ops
| Function | WOQL JSON-LD type |
| --- | --- |
| `type_of/2` | `TypeOf` |
| `isa/2` | `IsA` |
| `sub/2` (alias `subsumption/2`) | `Subsumption` |
| `cast/3` (alias `typecast/3`) | `Typecast` |
### Arithmetic
| Function | WOQL JSON-LD type |
| --- | --- |
| `eval/2` | `Eval` |
| `plus/1`, `minus/1`, `times/1`, `divide/1` | `Plus`/`Minus`/`Times`/`Divide` |
| `div/1` | `Div` |
| `exp/2` | `Exp` |
| `floor/1` | `Floor` |
| `sum/2` | `Sum` |
### String ops
| Function | WOQL JSON-LD type |
| --- | --- |
| `concat/2` (alias `concatenate/2`) | `Concatenate` |
| `join/3` | `Join` |
| `substr/5` (alias `substring/5`) | `Substring` |
| `trim/2` | `Trim` |
| `upper/2` | `Upper` |
| `lower/2` | `Lower` |
| `pad/4` | `Pad` |
| `split/3` | `Split` |
| `length/2` | `Length` |
| `regexp/3` | `Regexp` |
### List / Set / Dict ops
| Function | WOQL JSON-LD type |
| --- | --- |
| `dot/3` | `Dot` |
| `member/2` | `Member` |
| `slice/4` | `Slice` |
| `set_difference/3` | `SetDifference` |
| `set_intersection/3` | `SetIntersection` |
| `set_union/3` | `SetUnion` |
| `set_member/2` | `SetMember` |
| `list_to_set/2` | `ListToSet` |
### Path / navigation
| Function | WOQL JSON-LD type |
| --- | --- |
| `path/3`, `path/4` | `Path` |
Path patterns accept both string patterns (`path("v:S", "friend*", "v:O")`)
and structured builders via `TerminusDB.WOQL.Path` (`path_star/1`,
`path_plus/1`, `path_times/3`, `path_seq/1`, `path_or/1`, `path_inverse/1`,
`path_pred/1`, `path_any/0`).
### ID generation
| Function | WOQL JSON-LD type |
| --- | --- |
| `unique/3` | `HashKey` |
| `idgen/3` (alias `idgenerator/3`) | `LexicalKey` |
| `idgen_random/2` (alias `random_idgen/2`) | `RandomKey` |
### Documents
| Function | WOQL JSON-LD type |
| --- | --- |
| `read_document/2` | `ReadDocument` |
| `insert_document/2` | `InsertDocument` |
| `update_document/2` | `UpdateDocument` |
| `delete_document/1` | `DeleteDocument` |
### Graph context
| Function | WOQL JSON-LD type |
| --- | --- |
| `using/2` | `Using` |
| `from/2` | `From` |
| `into/2` | `Into` |
| `comment/2` | `Comment` |
### Graph meta
| Function | WOQL JSON-LD type |
| --- | --- |
| `size/2` | `Size` |
| `triple_count/2` | `TripleCount` |
### Range queries
| Function | WOQL JSON-LD type |
| --- | --- |
| `triple_slice/5`, `quad_slice/6` | `TripleSlice` |
| `triple_slice_rev/5`, `quad_slice_rev/6` | `TripleSliceRev` |
| `triple_next/4`, `quad_next/5` | `TripleNext` |
| `triple_previous/4`, `quad_previous/5` | `TriplePrevious` |
### Temporal / Allen interval algebra
| Function | WOQL JSON-LD type |
| --- | --- |
| `interval/3` | `Interval` |
| `interval_start_duration/3` | `IntervalStartDuration` |
| `interval_duration_end/3` | `IntervalDurationEnd` |
| `interval_relation/5` | `IntervalRelation` |
| `interval_relation_typed/3` | `IntervalRelationTyped` |
| `date_duration/3` | `DateDuration` |
| `day_after/2` | `DayAfter` |
| `day_before/2` | `DayBefore` |
| `weekday/2` | `Weekday` |
| `weekday_sunday_start/2` | `WeekdaySundayStart` |
| `iso_week/3` | `IsoWeek` |
| `month_start_date/2` | `MonthStartDate` |
| `month_end_date/2` | `MonthEndDate` |
| `month_start_dates/3` | `MonthStartDates` |
| `month_end_dates/3` | `MonthEndDates` |
| `in_range/3` | `InRange` |
| `sequence/5` | `Sequence` |
| `range_min/2` | `RangeMin` |
| `range_max/2` | `RangeMax` |
### CSV / IO
| Function | WOQL JSON-LD type |
| --- | --- |
| `get/2` | `Get` |
| `put/3` | `Put` |
| `woql_as/1` | `Column`/`Indicator` (helper) |
| `file/2` | `QueryResource` |
| `remote/2` | `QueryResource` |
| `post/2` | `QueryResource` |
### RDF list library
See `TerminusDB.WOQL.RDFList` for 17 RDF list manipulation functions.
### Literal / value helpers
| Function | Description |
| --- | --- |
| `var/1` | Wraps a name as `"v:Name"` |
| `iri/1` | Wraps a string as a `NodeValue` IRI |
| `string/1` | Wraps as `xsd:string` literal |
| `boolean/1` | Wraps as `xsd:boolean` literal |
| `datetime/1` | Wraps as `xsd:dateTime` literal |
| `date/1` | Wraps as `xsd:date` literal |
| `literal/2` | Generic typed literal |
| `true_/0` | `True` constant |
## Quick start
import TerminusDB.WOQL
query =
and_([
triple("v:Person", "rdf:type", iri("@schema:Person")),
triple("v:Person", "name", "v:Name")
])
jsonld = TerminusDB.WOQL.to_jsonld(query)
# Execute against a database
config = TerminusDB.Config.new(endpoint: "http://localhost:6363")
config = TerminusDB.Config.with_database(config, "mydb")
{:ok, result} = TerminusDB.WOQL.execute(config, query)
"""
alias TerminusDB.{Client, Config, Error}
alias TerminusDB.Client.Params
alias TerminusDB.WOQL.{Decoder, Encoder, Literal, Path}
defstruct [:op, :args]
@type t :: %__MODULE__{op: atom(), args: [term()]}
@type woql_var :: String.t()
@type woql_node :: String.t() | woql_var()
@type value :: String.t() | woql_var() | number() | boolean() | map() | [value()]
# --------------------------------------------------------------------------
# Query builders
# --------------------------------------------------------------------------
@doc """
Builds a `Triple` pattern: subject, predicate, object.
Any argument can be a variable (`"v:Name"`) or a constant.
## Examples
iex> q = TerminusDB.WOQL.triple("v:Person", "name", "v:Name")
iex> q.op
:triple
"""
@spec triple(woql_node(), woql_node(), value()) :: t()
def triple(subject, predicate, object) do
%__MODULE__{op: :triple, args: [subject, predicate, object]}
end
@doc """
Builds an `And` conjunction of sub-queries.
## Examples
iex> q = TerminusDB.WOQL.and_([
...> TerminusDB.WOQL.triple("v:S", "rdf:type", "v:T"),
...> TerminusDB.WOQL.eq("v:T", "Person")
...> ])
iex> q.op
:and
"""
@spec and_([t()]) :: t()
def and_(queries) when is_list(queries) do
%__MODULE__{op: :and, args: queries}
end
@doc """
Builds an `Or` disjunction of sub-queries.
## Examples
iex> q = TerminusDB.WOQL.or_([
...> TerminusDB.WOQL.eq("v:Name", "Alice"),
...> TerminusDB.WOQL.eq("v:Name", "Bob")
...> ])
iex> q.op
:or
"""
@spec or_([t()]) :: t()
def or_(queries) when is_list(queries) do
%__MODULE__{op: :or, args: queries}
end
@doc """
Builds an `Equals` unification: left equals right.
## Examples
iex> q = TerminusDB.WOQL.eq("v:Name", "Alice")
iex> q.op
:eq
"""
@spec eq(value(), value()) :: t()
def eq(left, right) do
%__MODULE__{op: :eq, args: [left, right]}
end
@doc """
Builds a `Select` that projects the given variables from a sub-query.
`vars` is a list of variable names (e.g. `["v:Name", "v:Person"]`).
## Examples
iex> q = TerminusDB.WOQL.select(["v:Name"],
...> TerminusDB.WOQL.and_([
...> TerminusDB.WOQL.triple("v:Person", "name", "v:Name")
...> ])
...> )
iex> q.op
:select
"""
@spec select([woql_var()], t()) :: t()
def select(vars, query) when is_list(vars) and is_struct(query, __MODULE__) do
%__MODULE__{op: :select, args: [vars, query]}
end
@doc """
Builds a `ReadDocument` that reads a document by ID into a variable.
## Examples
iex> q = TerminusDB.WOQL.read_document("Person/Alice", "v:Doc")
iex> q.op
:read_document
"""
@spec read_document(String.t(), woql_var()) :: t()
def read_document(id, var) do
%__MODULE__{op: :read_document, args: [id, var]}
end
@doc """
Builds a `TypeOf` that unifies the type of a node with a variable.
## Examples
iex> q = TerminusDB.WOQL.type_of("v:Person", "v:Type")
iex> q.op
:type_of
"""
@spec type_of(woql_node(), woql_var()) :: t()
def type_of(node, var) do
%__MODULE__{op: :type_of, args: [node, var]}
end
@doc """
Wraps a string as a `NodeValue` IRI — use for triple objects that should be
treated as IRIs rather than string literals.
## Examples
iex> TerminusDB.WOQL.iri("@schema:Person")
%{"@type" => "NodeValue", "node" => "@schema:Person"}
"""
@spec iri(String.t()) :: map()
def iri(node), do: Literal.iri(node)
# --------------------------------------------------------------------------
# Literal / value helpers
# --------------------------------------------------------------------------
@doc """
Wraps a name as a WOQL variable string (`"v:Name"`).
## Examples
iex> TerminusDB.WOQL.var("Person")
"v:Person"
"""
@spec var(String.t()) :: String.t()
def var(name), do: Literal.var(name)
@doc """
Wraps a string as an `xsd:string` literal dict.
## Examples
iex> TerminusDB.WOQL.string("hello")
%{"@type" => "xsd:string", "@value" => "hello"}
"""
@spec string(String.t()) :: map()
def string(value), do: Literal.string(value)
@doc """
Wraps a boolean as an `xsd:boolean` literal dict.
## Examples
iex> TerminusDB.WOQL.boolean(true)
%{"@type" => "xsd:boolean", "@value" => true}
"""
@spec boolean(boolean()) :: map()
def boolean(value), do: Literal.boolean(value)
@doc """
Wraps a `DateTime`, `NaiveDateTime`, or ISO 8601 string as an
`xsd:dateTime` literal dict.
## Examples
iex> TerminusDB.WOQL.datetime("2026-01-15T10:30:00Z")
%{"@type" => "xsd:dateTime", "@value" => "2026-01-15T10:30:00Z"}
"""
@spec datetime(DateTime.t() | NaiveDateTime.t() | String.t()) :: map()
def datetime(value), do: Literal.datetime(value)
@doc """
Wraps a `Date` or ISO 8601 string as an `xsd:date` literal dict.
## Examples
iex> TerminusDB.WOQL.date("2026-01-15")
%{"@type" => "xsd:date", "@value" => "2026-01-15"}
"""
@spec date(Date.t() | String.t()) :: map()
def date(value), do: Literal.date(value)
@doc """
Wraps a value as a typed literal dict. The type is prefixed with `xsd:` if it
does not already contain a colon.
## Examples
iex> TerminusDB.WOQL.literal("42", "integer")
%{"@type" => "xsd:integer", "@value" => "42"}
"""
@spec literal(term(), String.t()) :: map()
def literal(value, type), do: Literal.literal(value, type)
@doc """
Builds a `True` constant query.
## Examples
iex> q = TerminusDB.WOQL.true_()
iex> q.op
:true
"""
@spec true_ :: t()
def true_, do: %__MODULE__{op: true, args: []}
# --------------------------------------------------------------------------
# Logical combinators
# --------------------------------------------------------------------------
@doc """
Builds a `Not` negation of a sub-query.
## Examples
iex> q = TerminusDB.WOQL.not_(TerminusDB.WOQL.eq("v:N", "Alice"))
iex> q.op
:not
"""
@spec not_(t()) :: t()
def not_(query) when is_struct(query, __MODULE__) do
%__MODULE__{op: :not, args: [query]}
end
@doc """
Builds an `Optional` wrapper — the sub-query is allowed to fail without
invalidating the enclosing query.
## Examples
iex> q = TerminusDB.WOQL.opt(TerminusDB.WOQL.triple("v:S", "p", "v:O"))
iex> q.op
:opt
"""
@spec opt(t()) :: t()
def opt(query) when is_struct(query, __MODULE__) do
%__MODULE__{op: :opt, args: [query]}
end
@doc """
Alias for `opt/1`.
"""
@spec optional(t()) :: t()
def optional(query), do: opt(query)
@doc """
Builds a `Once` — obtain only one result from the sub-query.
## Examples
iex> q = TerminusDB.WOQL.once(TerminusDB.WOQL.triple("v:S", "p", "v:O"))
iex> q.op
:once
"""
@spec once(t()) :: t()
def once(query) when is_struct(query, __MODULE__) do
%__MODULE__{op: :once, args: [query]}
end
@doc """
Builds an `Immediately` — run side-effects without backtracking.
## Examples
iex> q = TerminusDB.WOQL.immediately(TerminusDB.WOQL.triple("v:S", "p", "v:O"))
iex> q.op
:immediately
"""
@spec immediately(t()) :: t()
def immediately(query) when is_struct(query, __MODULE__) do
%__MODULE__{op: :immediately, args: [query]}
end
# --------------------------------------------------------------------------
# Query modifiers
# --------------------------------------------------------------------------
@doc """
Builds a `Distinct` — returns distinct solutions for the given variables.
## Examples
iex> q = TerminusDB.WOQL.distinct(["v:Name"], TerminusDB.WOQL.triple("v:P", "name", "v:Name"))
iex> q.op
:distinct
"""
@spec distinct([woql_var()], t()) :: t()
def distinct(vars, query) when is_list(vars) and is_struct(query, __MODULE__) do
%__MODULE__{op: :distinct, args: [vars, query]}
end
@doc """
Builds a `Limit` — maximum number of results.
## Examples
iex> q = TerminusDB.WOQL.limit(10, TerminusDB.WOQL.triple("v:S", "p", "v:O"))
iex> q.op
:limit
"""
@spec limit(non_neg_integer(), t()) :: t()
def limit(n, query) when is_integer(n) and n >= 0 and is_struct(query, __MODULE__) do
%__MODULE__{op: :limit, args: [n, query]}
end
@doc """
Builds a `Start` — offset (start index) for results.
## Examples
iex> q = TerminusDB.WOQL.start(5, TerminusDB.WOQL.triple("v:S", "p", "v:O"))
iex> q.op
:start
"""
@spec start(non_neg_integer(), t()) :: t()
def start(n, query) when is_integer(n) and n >= 0 and is_struct(query, __MODULE__) do
%__MODULE__{op: :start, args: [n, query]}
end
@doc """
Builds an `OrderBy` — orders results by the given variables.
Accepts both tuple-list and keyword-list forms:
# Tuple list
TerminusDB.WOQL.order_by([{"v:Time", :asc}, {"v:Name", :desc}], query)
# Keyword list
TerminusDB.WOQL.order_by([time: :asc, name: :desc], query)
## Examples
iex> q = TerminusDB.WOQL.order_by([{"v:Name", :asc}], TerminusDB.WOQL.triple("v:S", "name", "v:Name"))
iex> q.op
:order_by
iex> q2 = TerminusDB.WOQL.order_by([name: :desc], TerminusDB.WOQL.triple("v:S", "name", "v:Name"))
iex> q2.op
:order_by
"""
@spec order_by([{String.t(), :asc | :desc}] | keyword(), t()) :: t()
def order_by(specs, query) when is_list(specs) and is_struct(query, __MODULE__) do
%__MODULE__{op: :order_by, args: [normalize_order_specs(specs), query]}
end
@doc """
Builds a `GroupBy` — groups sub-query results by `vars` using `template`,
binding into `grouped`.
## Examples
iex> q = TerminusDB.WOQL.group_by(["v:Type"], "v:Template", "v:Grouped",
...> TerminusDB.WOQL.triple("v:S", "rdf:type", "v:Type"))
iex> q.op
:group_by
"""
@spec group_by([woql_var()], value(), value(), t()) :: t()
def group_by(vars, template, grouped, query)
when is_list(vars) and is_struct(query, __MODULE__) do
%__MODULE__{op: :group_by, args: [vars, template, grouped, query]}
end
@doc """
Builds a `Count` — counts solutions of the sub-query and binds to `countvar`.
## Examples
iex> q = TerminusDB.WOQL.count("v:N", TerminusDB.WOQL.triple("v:S", "p", "v:O"))
iex> q.op
:count
"""
@spec count(woql_var(), t()) :: t()
def count(countvar, query) when is_binary(countvar) and is_struct(query, __MODULE__) do
%__MODULE__{op: :count, args: [countvar, query]}
end
@doc """
Builds a `Collect` — collects all solutions into a list.
## Examples
iex> q = TerminusDB.WOQL.collect("v:Template", "v:Into",
...> TerminusDB.WOQL.triple("v:S", "p", "v:O"))
iex> q.op
:collect
"""
@spec collect(value(), value(), t()) :: t()
def collect(template, into, query) when is_struct(query, __MODULE__) do
%__MODULE__{op: :collect, args: [template, into, query]}
end
@doc """
Builds a `star` query — selects everything as triples with default
variables `v:Subject`, `v:Predicate`, `v:Object`.
## Examples
iex> q = TerminusDB.WOQL.star()
iex> q.op
:triple
"""
@spec star() :: t()
def star, do: triple("v:Subject", "v:Predicate", "v:Object")
@doc """
Alias for `star/0`.
"""
@spec all() :: t()
def all, do: star()
# --------------------------------------------------------------------------
# Graph patterns
# --------------------------------------------------------------------------
@doc """
Builds a `Quad` pattern: subject, predicate, object, graph.
Quads reuse the `Triple` JSON-LD type with an added `graph` field.
## Examples
iex> q = TerminusDB.WOQL.quad("v:S", "name", "v:N", "instance")
iex> q.op
:quad
"""
@spec quad(woql_node(), woql_node(), value(), String.t()) :: t()
def quad(s, p, o, graph) do
%__MODULE__{op: :quad, args: [s, p, o, graph]}
end
@doc """
Builds an `AddedTriple` — a triple added in the current commit.
## Examples
iex> q = TerminusDB.WOQL.added_triple("v:S", "name", "v:N")
iex> q.op
:added_triple
"""
@spec added_triple(woql_node(), woql_node(), value()) :: t()
def added_triple(s, p, o) do
%__MODULE__{op: :added_triple, args: [s, p, o]}
end
@doc """
Builds a `DeletedTriple` — a triple removed in the current commit.
## Examples
iex> q = TerminusDB.WOQL.removed_triple("v:S", "name", "v:N")
iex> q.op
:removed_triple
"""
@spec removed_triple(woql_node(), woql_node(), value()) :: t()
def removed_triple(s, p, o) do
%__MODULE__{op: :removed_triple, args: [s, p, o]}
end
@doc """
Builds an `AddedTriple` with a graph — a quad added in the current commit.
## Examples
iex> q = TerminusDB.WOQL.added_quad("v:S", "name", "v:N", "instance")
iex> q.op
:added_quad
"""
@spec added_quad(woql_node(), woql_node(), value(), String.t()) :: t()
def added_quad(s, p, o, graph) do
%__MODULE__{op: :added_quad, args: [s, p, o, graph]}
end
@doc """
Builds a `DeletedTriple` with a graph — a quad removed in the current commit.
## Examples
iex> q = TerminusDB.WOQL.removed_quad("v:S", "name", "v:N", "instance")
iex> q.op
:removed_quad
"""
@spec removed_quad(woql_node(), woql_node(), value(), String.t()) :: t()
def removed_quad(s, p, o, graph) do
%__MODULE__{op: :removed_quad, args: [s, p, o, graph]}
end
@doc """
Builds an `AddTriple` — adds triples matching `[S, P, O]`.
## Examples
iex> q = TerminusDB.WOQL.add_triple("v:S", "name", "Alice")
iex> q.op
:add_triple
"""
@spec add_triple(woql_node(), woql_node(), value()) :: t()
def add_triple(s, p, o) do
%__MODULE__{op: :add_triple, args: [s, p, o]}
end
@doc """
Builds a `DeleteTriple` — deletes triples matching `[S, P, O]`.
## Examples
iex> q = TerminusDB.WOQL.delete_triple("v:S", "name", "v:O")
iex> q.op
:delete_triple
"""
@spec delete_triple(woql_node(), woql_node(), value()) :: t()
def delete_triple(s, p, o) do
%__MODULE__{op: :delete_triple, args: [s, p, o]}
end
@doc """
Builds an `AddTriple` with a graph — adds quads.
## Examples
iex> q = TerminusDB.WOQL.add_quad("v:S", "name", "Alice", "instance")
iex> q.op
:add_quad
"""
@spec add_quad(woql_node(), woql_node(), value(), String.t()) :: t()
def add_quad(s, p, o, graph) do
%__MODULE__{op: :add_quad, args: [s, p, o, graph]}
end
@doc """
Builds a `DeleteTriple` with a graph — deletes quads.
## Examples
iex> q = TerminusDB.WOQL.delete_quad("v:S", "name", "v:O", "instance")
iex> q.op
:delete_quad
"""
@spec delete_quad(woql_node(), woql_node(), value(), String.t()) :: t()
def delete_quad(s, p, o, graph) do
%__MODULE__{op: :delete_quad, args: [s, p, o, graph]}
end
@doc """
Composes an update: optionally delete any existing triple, then add the new
one. Equivalent to `and_([opt(delete_triple(s, p, "v:OldObject")), add_triple(s, p, o)])`.
The internal variable `"v:OldObject"` is used to match any existing object
before deletion. Avoid using `"v:OldObject"` as a variable in surrounding
queries to prevent unintended unification.
## Examples
iex> q = TerminusDB.WOQL.update_triple("v:S", "name", "Alice")
iex> q.op
:and
"""
@spec update_triple(woql_node(), woql_node(), value()) :: t()
def update_triple(s, p, o) do
and_([opt(delete_triple(s, p, "v:OldObject")), add_triple(s, p, o)])
end
@doc """
Composes an update: optionally delete any existing quad, then add the new
one.
The internal variable `"v:OldObject"` is used to match any existing object
before deletion. Avoid using `"v:OldObject"` as a variable in surrounding
queries to prevent unintended unification.
## Examples
iex> q = TerminusDB.WOQL.update_quad("v:S", "name", "Alice", "instance")
iex> q.op
:and
"""
@spec update_quad(woql_node(), woql_node(), value(), String.t()) :: t()
def update_quad(s, p, o, graph) do
and_([opt(delete_quad(s, p, "v:OldObject", graph)), add_quad(s, p, o, graph)])
end
# --------------------------------------------------------------------------
# Comparison
# --------------------------------------------------------------------------
@doc """
Builds a `Less` comparison: `left < right`.
## Examples
iex> q = TerminusDB.WOQL.less("v:Age", 30)
iex> q.op
:less
"""
@spec less(value(), value()) :: t()
def less(left, right) do
%__MODULE__{op: :less, args: [left, right]}
end
@doc """
Builds a `Greater` comparison: `left > right`.
## Examples
iex> q = TerminusDB.WOQL.greater("v:Age", 30)
iex> q.op
:greater
"""
@spec greater(value(), value()) :: t()
def greater(left, right) do
%__MODULE__{op: :greater, args: [left, right]}
end
@doc """
Builds a `Gte` comparison: `left >= right`.
## Examples
iex> q = TerminusDB.WOQL.gte("v:Age", 30)
iex> q.op
:gte
"""
@spec gte(value(), value()) :: t()
def gte(left, right) do
%__MODULE__{op: :gte, args: [left, right]}
end
@doc """
Builds an `Lte` comparison: `left <= right`.
## Examples
iex> q = TerminusDB.WOQL.lte("v:Age", 30)
iex> q.op
:lte
"""
@spec lte(value(), value()) :: t()
def lte(left, right) do
%__MODULE__{op: :lte, args: [left, right]}
end
@doc """
Builds a `Like` — string similarity with edit-distance `dist`.
## Examples
iex> q = TerminusDB.WOQL.like("v:Name", "Alice", 2)
iex> q.op
:like
"""
@spec like(value(), value(), value()) :: t()
def like(left, right, dist) do
%__MODULE__{op: :like, args: [left, right, dist]}
end
# --------------------------------------------------------------------------
# Schema ops
# --------------------------------------------------------------------------
@doc """
Builds an `IsA` — true if `element` is a member of `of_type`.
## Examples
iex> q = TerminusDB.WOQL.isa("v:X", WOQL.iri("@schema:Person"))
iex> q.op
:isa
"""
@spec isa(woql_node(), woql_node()) :: t()
def isa(element, of_type) do
%__MODULE__{op: :isa, args: [element, of_type]}
end
@doc """
Builds a `Subsumption` — true if `child` is a subclass of `parent`.
## Examples
iex> q = TerminusDB.WOQL.sub(WOQL.iri("@schema:Animal"), WOQL.iri("@schema:Dog"))
iex> q.op
:sub
"""
@spec sub(woql_node(), woql_node()) :: t()
def sub(parent, child) do
%__MODULE__{op: :sub, args: [parent, child]}
end
@doc """
Alias for `sub/2`.
"""
@spec subsumption(woql_node(), woql_node()) :: t()
def subsumption(parent, child), do: sub(parent, child)
@doc """
Builds a `Typecast` — casts `value` to `type`, binding to `result`.
## Examples
iex> q = TerminusDB.WOQL.cast("v:Val", "xsd:integer", "v:Result")
iex> q.op
:cast
"""
@spec cast(value(), value(), value()) :: t()
def cast(value, type, result) do
%__MODULE__{op: :cast, args: [value, type, result]}
end
@doc """
Alias for `cast/3`.
"""
@spec typecast(value(), value(), value()) :: t()
def typecast(value, type, result), do: cast(value, type, result)
# --------------------------------------------------------------------------
# Arithmetic
# --------------------------------------------------------------------------
@doc """
Builds an `Eval` — evaluates an arithmetic expression and binds to `result`.
The `expression` is typically a nested arithmetic query such as `plus/1`,
`minus/1`, `times/1`, etc.
## Examples
iex> q = TerminusDB.WOQL.eval(TerminusDB.WOQL.plus(["v:X", 5]), "v:Result")
iex> q.op
:eval
"""
@spec eval(t(), value()) :: t()
def eval(expression, result) when is_struct(expression, __MODULE__) do
%__MODULE__{op: :eval, args: [expression, result]}
end
@doc """
Builds a `Plus` — sums a list of arithmetic values.
## Examples
iex> q = TerminusDB.WOQL.plus(["v:X", 5, "v:Y"])
iex> q.op
:plus
"""
@spec plus([value() | t()]) :: t()
def plus(args) when is_list(args) do
%__MODULE__{op: :plus, args: args}
end
@doc """
Builds a `Minus` — subtracts a list of arithmetic values left-to-right.
## Examples
iex> q = TerminusDB.WOQL.minus(["v:X", 5])
iex> q.op
:minus
"""
@spec minus([value() | t()]) :: t()
def minus(args) when is_list(args) do
%__MODULE__{op: :minus, args: args}
end
@doc """
Builds a `Times` — multiplies a list of arithmetic values.
## Examples
iex> q = TerminusDB.WOQL.times(["v:X", 5])
iex> q.op
:times
"""
@spec times([value() | t()]) :: t()
def times(args) when is_list(args) do
%__MODULE__{op: :times, args: args}
end
@doc """
Builds a `Divide` — divides a list of arithmetic values left-to-right.
## Examples
iex> q = TerminusDB.WOQL.divide(["v:X", 5])
iex> q.op
:divide
"""
@spec divide([value() | t()]) :: t()
def divide(args) when is_list(args) do
%__MODULE__{op: :divide, args: args}
end
@doc """
Builds a `Div` — integer division of a list of arithmetic values.
## Examples
iex> q = TerminusDB.WOQL.div(["v:X", 5])
iex> q.op
:div
"""
@spec div([value() | t()]) :: t()
def div(args) when is_list(args) do
%__MODULE__{op: :div, args: args}
end
@doc """
Builds an `Exp` — `base` raised to the power of `exponent`.
## Examples
iex> q = TerminusDB.WOQL.exp("v:X", 2)
iex> q.op
:exp
"""
@spec exp(value() | t(), value() | t()) :: t()
def exp(base, exponent) do
%__MODULE__{op: :exp, args: [base, exponent]}
end
@doc """
Builds a `Floor` — greatest integer ≤ `value`.
## Examples
iex> q = TerminusDB.WOQL.floor("v:X")
iex> q.op
:floor
"""
@spec floor(value() | t()) :: t()
def floor(value) do
%__MODULE__{op: :floor, args: [value]}
end
@doc """
Builds a `Sum` — sums a list of numbers into a single value.
## Examples
iex> q = TerminusDB.WOQL.sum("v:List", "v:Result")
iex> q.op
:sum
"""
@spec sum(value(), value()) :: t()
def sum(list, result) do
%__MODULE__{op: :sum, args: [list, result]}
end
# --------------------------------------------------------------------------
# String ops
# --------------------------------------------------------------------------
@doc """
Builds a `Concatenate` — concatenates a list of strings/vars into `result`.
## Examples
iex> q = TerminusDB.WOQL.concat(["v:First", " ", "v:Last"], "v:Full")
iex> q.op
:concat
"""
@spec concat([value()], value()) :: t()
def concat(list, result) when is_list(list) do
%__MODULE__{op: :concat, args: [list, result]}
end
@doc """
Alias for `concat/2`.
"""
@spec concatenate([value()], value()) :: t()
def concatenate(list, result), do: concat(list, result)
@doc """
Builds a `Join` — joins a list into a string with `glue`.
## Examples
iex> q = TerminusDB.WOQL.join("v:List", ", ", "v:Result")
iex> q.op
:join
"""
@spec join(value(), value(), value()) :: t()
def join(list, glue, output) do
%__MODULE__{op: :join, args: [list, glue, output]}
end
@doc """
Builds a `Substring` — extracts a substring with `before`/`length`/`after`.
## Examples
iex> q = TerminusDB.WOQL.substr("v:String", 5, "v:Sub", 0, 0)
iex> q.op
:substr
"""
@spec substr(value(), value(), value(), value(), value()) :: t()
def substr(string, length, substring, before \\ 0, after_ \\ 0) do
%__MODULE__{op: :substr, args: [string, length, substring, before, after_]}
end
@doc """
Alias for `substr/5`.
"""
@spec substring(value(), value(), value(), value(), value()) :: t()
def substring(string, length, substring, before \\ 0, after_ \\ 0),
do: substr(string, length, substring, before, after_)
@doc """
Builds a `Trim` — strips leading/trailing whitespace.
## Examples
iex> q = TerminusDB.WOQL.trim("v:Untrimmed", "v:Trimmed")
iex> q.op
:trim
"""
@spec trim(value(), value()) :: t()
def trim(untrimmed, trimmed) do
%__MODULE__{op: :trim, args: [untrimmed, trimmed]}
end
@doc """
Builds an `Upper` — converts to uppercase.
## Examples
iex> q = TerminusDB.WOQL.upper("v:Input", "v:Result")
iex> q.op
:upper
"""
@spec upper(value(), value()) :: t()
def upper(left, right) do
%__MODULE__{op: :upper, args: [left, right]}
end
@doc """
Builds a `Lower` — converts to lowercase.
## Examples
iex> q = TerminusDB.WOQL.lower("v:Input", "v:Result")
iex> q.op
:lower
"""
@spec lower(value(), value()) :: t()
def lower(left, right) do
%__MODULE__{op: :lower, args: [left, right]}
end
@doc """
Builds a `Pad` — pads string to `length` with `pad`.
## Examples
iex> q = TerminusDB.WOQL.pad("v:Input", "0", 10, "v:Result")
iex> q.op
:pad
"""
@spec pad(value(), value(), value(), value()) :: t()
def pad(input, pad, length, output) do
%__MODULE__{op: :pad, args: [input, pad, length, output]}
end
@doc """
Builds a `Split` — splits a string by `glue` into a list.
## Examples
iex> q = TerminusDB.WOQL.split("v:String", ",", "v:Result")
iex> q.op
:split
"""
@spec split(value(), value(), value()) :: t()
def split(input, glue, output) do
%__MODULE__{op: :split, args: [input, glue, output]}
end
@doc """
Builds a `Length` — binds the length of a list.
## Examples
iex> q = TerminusDB.WOQL.length("v:List", "v:Len")
iex> q.op
:length
"""
@spec length(value(), value()) :: t()
def length(list, len) do
%__MODULE__{op: :length, args: [list, len]}
end
@doc """
Builds a `Regexp` — regex match; `result_list` captures groups.
## Examples
iex> q = TerminusDB.WOQL.regexp("pattern", "v:String", "v:Result")
iex> q.op
:regexp
"""
@spec regexp(value(), value(), value()) :: t()
def regexp(pattern, string, result_list) do
%__MODULE__{op: :regexp, args: [pattern, string, result_list]}
end
# --------------------------------------------------------------------------
# List / Set / Dict ops
# --------------------------------------------------------------------------
@doc """
Builds a `Dot` — accesses a dictionary field or list element.
## Examples
iex> q = TerminusDB.WOQL.dot("v:Doc", "field", "v:Value")
iex> q.op
:dot
"""
@spec dot(value(), value(), value()) :: t()
def dot(document, field, value) do
%__MODULE__{op: :dot, args: [document, field, value]}
end
@doc """
Builds a `Member` — iterates members of a list.
## Examples
iex> q = TerminusDB.WOQL.member("v:Item", "v:List")
iex> q.op
:member
"""
@spec member(value(), value()) :: t()
def member(item, list) do
%__MODULE__{op: :member, args: [item, list]}
end
@doc """
Builds a `Slice` — slices a list `[start, end)`.
## Examples
iex> q = TerminusDB.WOQL.slice("v:List", "v:Result", 0, 5)
iex> q.op
:slice
"""
@spec slice(value(), value(), value(), value() | nil) :: t()
def slice(input, result, start, end_val \\ nil) do
%__MODULE__{op: :slice, args: [input, result, start, end_val]}
end
@doc """
Builds a `SetDifference`.
## Examples
iex> q = TerminusDB.WOQL.set_difference("v:A", "v:B", "v:Result")
iex> q.op
:set_difference
"""
@spec set_difference(value(), value(), value()) :: t()
def set_difference(list_a, list_b, result) do
%__MODULE__{op: :set_difference, args: [list_a, list_b, result]}
end
@doc """
Builds a `SetIntersection`.
## Examples
iex> q = TerminusDB.WOQL.set_intersection("v:A", "v:B", "v:Result")
iex> q.op
:set_intersection
"""
@spec set_intersection(value(), value(), value()) :: t()
def set_intersection(list_a, list_b, result) do
%__MODULE__{op: :set_intersection, args: [list_a, list_b, result]}
end
@doc """
Builds a `SetUnion`.
## Examples
iex> q = TerminusDB.WOQL.set_union("v:A", "v:B", "v:Result")
iex> q.op
:set_union
"""
@spec set_union(value(), value(), value()) :: t()
def set_union(list_a, list_b, result) do
%__MODULE__{op: :set_union, args: [list_a, list_b, result]}
end
@doc """
Builds a `SetMember` — membership test in a set.
## Examples
iex> q = TerminusDB.WOQL.set_member("v:Item", "v:Set")
iex> q.op
:set_member
"""
@spec set_member(value(), value()) :: t()
def set_member(element, set) do
%__MODULE__{op: :set_member, args: [element, set]}
end
@doc """
Builds a `ListToSet` — converts a list to a set.
## Examples
iex> q = TerminusDB.WOQL.list_to_set("v:List", "v:Set")
iex> q.op
:list_to_set
"""
@spec list_to_set(value(), value()) :: t()
def list_to_set(input, result) do
%__MODULE__{op: :list_to_set, args: [input, result]}
end
# --------------------------------------------------------------------------
# Path / navigation
# --------------------------------------------------------------------------
@doc """
Builds a `Path` query — traverses the graph from `subject` to `object`
following the given `pattern`.
The pattern can be either a string (compiled via `TerminusDB.WOQL.Path`) or
an AST node built via the structured builders (`path_star/1`, `path_plus/1`,
etc.).
## String patterns
# Simple predicate
TerminusDB.WOQL.path("v:S", "friend", "v:O")
# Inverse traversal
TerminusDB.WOQL.path("v:S", "<friend", "v:O")
# Star (zero or more)
TerminusDB.WOQL.path("v:S", "friend*", "v:O")
# Plus (one or more)
TerminusDB.WOQL.path("v:S", "friend+", "v:O")
# Bounded repetition
TerminusDB.WOQL.path("v:S", "friend{1,3}", "v:O")
# Alternation
TerminusDB.WOQL.path("v:S", "friend|foe", "v:O")
# Sequence
TerminusDB.WOQL.path("v:S", "friend,location", "v:O")
# Any predicate
TerminusDB.WOQL.path("v:S", ".", "v:O")
# Grouping
TerminusDB.WOQL.path("v:S", "(friend|foe)*", "v:O")
## Structured patterns
TerminusDB.WOQL.path("v:S",
TerminusDB.WOQL.Path.path_star(TerminusDB.WOQL.Path.path_pred("friend")),
"v:O"
)
## Options
- A 4th argument binds the path itself to a variable.
## Examples
iex> q = TerminusDB.WOQL.path("v:S", "friend*", "v:O")
iex> q.op
:path
iex> q2 = TerminusDB.WOQL.path("v:S", "friend*", "v:O", "v:Path")
iex> q2.args
["v:S", {:star, {:pred, "friend"}}, "v:O", "v:Path"]
"""
@spec path(woql_node(), String.t() | tuple(), value()) :: t()
def path(subject, pattern, object) do
%__MODULE__{op: :path, args: [subject, Path.normalize(pattern), object]}
end
@spec path(woql_node(), String.t() | tuple(), value(), woql_var()) :: t()
def path(subject, pattern, object, path_var) do
%__MODULE__{op: :path, args: [subject, Path.normalize(pattern), object, path_var]}
end
# --------------------------------------------------------------------------
# ID generation
# --------------------------------------------------------------------------
@doc """
Builds a `HashKey` — deterministic hash ID from a key list.
## Examples
iex> q = TerminusDB.WOQL.unique("Person/", ["v:Name", "v:Email"], "v:ID")
iex> q.op
:unique
"""
@spec unique(String.t(), [value()], woql_var()) :: t()
def unique(prefix, key_list, uri) do
%__MODULE__{op: :unique, args: [prefix, key_list, uri]}
end
@doc """
Builds a `LexicalKey` — lexical (deterministic, non-hash) ID.
## Examples
iex> q = TerminusDB.WOQL.idgen("Person/", ["v:Name"], "v:ID")
iex> q.op
:idgen
"""
@spec idgen(String.t(), [value()], woql_var()) :: t()
def idgen(prefix, key_list, uri) do
%__MODULE__{op: :idgen, args: [prefix, key_list, uri]}
end
@doc """
Alias for `idgen/3`.
"""
@spec idgenerator(String.t(), [value()], woql_var()) :: t()
def idgenerator(prefix, key_list, uri), do: idgen(prefix, key_list, uri)
@doc """
Builds a `RandomKey` — cryptographically-secure random ID.
## Examples
iex> q = TerminusDB.WOQL.idgen_random("Person/", "v:ID")
iex> q.op
:idgen_random
"""
@spec idgen_random(String.t(), woql_var()) :: t()
def idgen_random(prefix, uri) do
%__MODULE__{op: :idgen_random, args: [prefix, uri]}
end
@doc """
Alias for `idgen_random/2`.
"""
@spec random_idgen(String.t(), woql_var()) :: t()
def random_idgen(prefix, uri), do: idgen_random(prefix, uri)
# --------------------------------------------------------------------------
# Document mutations
# --------------------------------------------------------------------------
@doc """
Builds an `InsertDocument` — inserts a document.
`identifier` is a variable (e.g. `"v:Id"`) that will be bound to the
inserted document's IRI. TerminusDB 12 requires it for well-formed
`InsertDocument` JSON-LD.
## Examples
iex> q = TerminusDB.WOQL.insert_document("v:Doc")
iex> q.op
:insert_document
iex> q = TerminusDB.WOQL.insert_document(%{"@type" => "Person"}, "v:Id")
iex> q.op
:insert_document
"""
@spec insert_document(value(), woql_node() | nil) :: t()
def insert_document(doc, identifier \\ nil) do
%__MODULE__{op: :insert_document, args: [doc, identifier]}
end
@doc """
Builds an `UpdateDocument` — insert-or-replace a document.
`identifier` is a variable (e.g. `"v:Id"`) that will be bound to the
updated document's IRI.
## Examples
iex> q = TerminusDB.WOQL.update_document("v:Doc")
iex> q.op
:update_document
iex> q = TerminusDB.WOQL.update_document(%{"@type" => "Person"}, "v:Id")
iex> q.op
:update_document
"""
@spec update_document(value(), woql_node() | nil) :: t()
def update_document(doc, identifier \\ nil) do
%__MODULE__{op: :update_document, args: [doc, identifier]}
end
@doc """
Builds a `DeleteDocument` — delete a document by IRI.
## Examples
iex> q = TerminusDB.WOQL.delete_document("Person/Alice")
iex> q.op
:delete_document
"""
@spec delete_document(woql_node()) :: t()
def delete_document(iri) do
%__MODULE__{op: :delete_document, args: [iri]}
end
# --------------------------------------------------------------------------
# Graph context
# --------------------------------------------------------------------------
@doc """
Builds a `Using` — scopes the enclosed query to a data product / collection.
## Examples
iex> q = TerminusDB.WOQL.using("mydb", TerminusDB.WOQL.triple("v:S", "p", "v:O"))
iex> q.op
:using
"""
@spec using(String.t(), t()) :: t()
def using(collection, query) when is_struct(query, __MODULE__) do
%__MODULE__{op: :using, args: [collection, query]}
end
@doc """
Builds a `From` — sets the default graph for the enclosed query.
## Examples
iex> q = TerminusDB.WOQL.from("instance", TerminusDB.WOQL.triple("v:S", "p", "v:O"))
iex> q.op
:from
"""
@spec from(String.t(), t()) :: t()
def from(graph, query) when is_struct(query, __MODULE__) do
%__MODULE__{op: :from, args: [graph, query]}
end
@doc """
Builds an `Into` — sets the output graph for writing.
## Examples
iex> q = TerminusDB.WOQL.into("schema", TerminusDB.WOQL.add_triple("v:S", "p", "v:O"))
iex> q.op
:into
"""
@spec into(String.t(), t()) :: t()
def into(graph, query) when is_struct(query, __MODULE__) do
%__MODULE__{op: :into, args: [graph, query]}
end
@doc """
Builds a `Comment` — attaches a text comment to a sub-query.
## Examples
iex> q = TerminusDB.WOQL.comment("find friends", TerminusDB.WOQL.triple("v:S", "p", "v:O"))
iex> q.op
:comment
"""
@spec comment(String.t(), t()) :: t()
def comment(text, query) when is_struct(query, __MODULE__) do
%__MODULE__{op: :comment, args: [text, query]}
end
# --------------------------------------------------------------------------
# Graph meta
# --------------------------------------------------------------------------
@doc """
Builds a `Size` — binds the size (bytes) of a graph.
## Examples
iex> q = TerminusDB.WOQL.size("instance", "v:Size")
iex> q.op
:size
"""
@spec size(String.t(), value()) :: t()
def size(graph, size_var) do
%__MODULE__{op: :size, args: [graph, size_var]}
end
@doc """
Builds a `TripleCount` — binds the number of triples in a graph.
## Examples
iex> q = TerminusDB.WOQL.triple_count("instance", "v:Count")
iex> q.op
:triple_count
"""
@spec triple_count(String.t(), value()) :: t()
def triple_count(graph, count_var) do
%__MODULE__{op: :triple_count, args: [graph, count_var]}
end
# --------------------------------------------------------------------------
# Range queries
# --------------------------------------------------------------------------
@doc """
Builds a `TripleSlice` — triple pattern with half-open value range
[low, high) on the object.
## Examples
iex> q = TerminusDB.WOQL.triple_slice("v:S", "v:P", "v:O", 10, 100)
iex> q.op
:triple_slice
"""
@spec triple_slice(woql_node(), woql_node(), value(), value(), value()) :: t()
def triple_slice(subject, predicate, object, low, high) do
%__MODULE__{op: :triple_slice, args: [subject, predicate, object, low, high]}
end
@doc """
Builds a `TripleSlice` with an explicit graph selector.
## Examples
iex> q = TerminusDB.WOQL.quad_slice("v:S", "v:P", "v:O", 10, 100, "instance")
iex> q.op
:triple_slice
"""
@spec quad_slice(woql_node(), woql_node(), value(), value(), value(), String.t()) :: t()
def quad_slice(subject, predicate, object, low, high, graph) do
%__MODULE__{op: :triple_slice, args: [subject, predicate, object, low, high, graph]}
end
@doc """
Builds a `TripleSliceRev` — same as `triple_slice/5` but iterates in
descending order (high to low).
## Examples
iex> q = TerminusDB.WOQL.triple_slice_rev("v:S", "v:P", "v:O", 10, 100)
iex> q.op
:triple_slice_rev
"""
@spec triple_slice_rev(woql_node(), woql_node(), value(), value(), value()) :: t()
def triple_slice_rev(subject, predicate, object, low, high) do
%__MODULE__{op: :triple_slice_rev, args: [subject, predicate, object, low, high]}
end
@doc """
Builds a `TripleSliceRev` with an explicit graph selector.
## Examples
iex> q = TerminusDB.WOQL.quad_slice_rev("v:S", "v:P", "v:O", 10, 100, "instance")
iex> q.op
:triple_slice_rev
"""
@spec quad_slice_rev(woql_node(), woql_node(), value(), value(), value(), String.t()) :: t()
def quad_slice_rev(subject, predicate, object, low, high, graph) do
%__MODULE__{op: :triple_slice_rev, args: [subject, predicate, object, low, high, graph]}
end
@doc """
Builds a `TripleNext` — finds the next object value after a reference.
When object is bound and next is free, finds the smallest next > object.
## Examples
iex> q = TerminusDB.WOQL.triple_next("v:S", "v:P", "v:O", "v:Next")
iex> q.op
:triple_next
"""
@spec triple_next(woql_node(), woql_node(), value(), value()) :: t()
def triple_next(subject, predicate, object, next_val) do
%__MODULE__{op: :triple_next, args: [subject, predicate, object, next_val]}
end
@doc """
Builds a `TripleNext` with an explicit graph selector.
## Examples
iex> q = TerminusDB.WOQL.quad_next("v:S", "v:P", "v:O", "v:Next", "instance")
iex> q.op
:triple_next
"""
@spec quad_next(woql_node(), woql_node(), value(), value(), String.t()) :: t()
def quad_next(subject, predicate, object, next_val, graph) do
%__MODULE__{op: :triple_next, args: [subject, predicate, object, next_val, graph]}
end
@doc """
Builds a `TriplePrevious` — finds the previous object value before a
reference. When object is bound and previous is free, finds the largest
previous < object.
## Examples
iex> q = TerminusDB.WOQL.triple_previous("v:S", "v:P", "v:O", "v:Prev")
iex> q.op
:triple_previous
"""
@spec triple_previous(woql_node(), woql_node(), value(), value()) :: t()
def triple_previous(subject, predicate, object, prev_val) do
%__MODULE__{op: :triple_previous, args: [subject, predicate, object, prev_val]}
end
@doc """
Builds a `TriplePrevious` with an explicit graph selector.
## Examples
iex> q = TerminusDB.WOQL.quad_previous("v:S", "v:P", "v:O", "v:Prev", "instance")
iex> q.op
:triple_previous
"""
@spec quad_previous(woql_node(), woql_node(), value(), value(), String.t()) :: t()
def quad_previous(subject, predicate, object, prev_val, graph) do
%__MODULE__{op: :triple_previous, args: [subject, predicate, object, prev_val, graph]}
end
# --------------------------------------------------------------------------
# Temporal / Allen interval algebra
# --------------------------------------------------------------------------
@doc """
Builds an `Interval` — constructs/deconstructs a half-open
`xdd:dateTimeInterval` [start, end).
## Examples
iex> q = TerminusDB.WOQL.interval("v:Start", "v:End", "v:I")
iex> q.op
:interval
"""
@spec interval(value(), value(), value()) :: t()
def interval(start_val, end_val, interval_val) do
%__MODULE__{op: :interval, args: [start_val, end_val, interval_val]}
end
@doc """
Builds an `IntervalStartDuration` — relates interval to start endpoint
and precise `xsd:duration`.
## Examples
iex> q = TerminusDB.WOQL.interval_start_duration("v:Start", "v:Dur", "v:I")
iex> q.op
:interval_start_duration
"""
@spec interval_start_duration(value(), value(), value()) :: t()
def interval_start_duration(start_val, duration, interval_val) do
%__MODULE__{op: :interval_start_duration, args: [start_val, duration, interval_val]}
end
@doc """
Builds an `IntervalDurationEnd` — relates interval to end endpoint
and precise `xsd:duration`.
## Examples
iex> q = TerminusDB.WOQL.interval_duration_end("v:Dur", "v:End", "v:I")
iex> q.op
:interval_duration_end
"""
@spec interval_duration_end(value(), value(), value()) :: t()
def interval_duration_end(duration, end_val, interval_val) do
%__MODULE__{op: :interval_duration_end, args: [duration, end_val, interval_val]}
end
@doc """
Builds an `IntervalRelation` — Allen's Interval Algebra: classifies
the relationship between two half-open intervals.
## Examples
iex> q = TerminusDB.WOQL.interval_relation("v:Rel", "v:XS", "v:XE", "v:YS", "v:YE")
iex> q.op
:interval_relation
"""
@spec interval_relation(value(), value(), value(), value(), value()) :: t()
def interval_relation(relation, x_start, x_end, y_start, y_end) do
%__MODULE__{op: :interval_relation, args: [relation, x_start, x_end, y_start, y_end]}
end
@doc """
Builds an `IntervalRelationTyped` — Allen's Interval Algebra on
`xdd:dateTimeInterval` values.
## Examples
iex> q = TerminusDB.WOQL.interval_relation_typed("v:Rel", "v:X", "v:Y")
iex> q.op
:interval_relation_typed
"""
@spec interval_relation_typed(value(), value(), value()) :: t()
def interval_relation_typed(relation, x, y) do
%__MODULE__{op: :interval_relation_typed, args: [relation, x, y]}
end
@doc """
Builds a `DateDuration` — tri-directional duration arithmetic for
dates/dateTimes (end-of-month preserving).
## Examples
iex> q = TerminusDB.WOQL.date_duration("v:Start", "v:End", "v:Dur")
iex> q.op
:date_duration
"""
@spec date_duration(value(), value(), value()) :: t()
def date_duration(start_val, end_val, duration) do
%__MODULE__{op: :date_duration, args: [start_val, end_val, duration]}
end
@doc """
Builds a `DayAfter` — computes the calendar day after the given date
(bidirectional).
## Examples
iex> q = TerminusDB.WOQL.day_after("v:Date", "v:Next")
iex> q.op
:day_after
"""
@spec day_after(value(), value()) :: t()
def day_after(date, next_date) do
%__MODULE__{op: :day_after, args: [date, next_date]}
end
@doc """
Builds a `DayBefore` — computes the calendar day before the given date
(bidirectional).
## Examples
iex> q = TerminusDB.WOQL.day_before("v:Date", "v:Prev")
iex> q.op
:day_before
"""
@spec day_before(value(), value()) :: t()
def day_before(date, previous) do
%__MODULE__{op: :day_before, args: [date, previous]}
end
@doc """
Builds a `Weekday` — computes ISO 8601 weekday number (Monday=1,
Sunday=7).
## Examples
iex> q = TerminusDB.WOQL.weekday("v:Date", "v:Day")
iex> q.op
:weekday
"""
@spec weekday(value(), value()) :: t()
def weekday(date, weekday_val) do
%__MODULE__{op: :weekday, args: [date, weekday_val]}
end
@doc """
Builds a `WeekdaySundayStart` — computes US-convention weekday
(Sunday=1, Saturday=7).
## Examples
iex> q = TerminusDB.WOQL.weekday_sunday_start("v:Date", "v:Day")
iex> q.op
:weekday_sunday_start
"""
@spec weekday_sunday_start(value(), value()) :: t()
def weekday_sunday_start(date, weekday_val) do
%__MODULE__{op: :weekday_sunday_start, args: [date, weekday_val]}
end
@doc """
Builds an `IsoWeek` — computes ISO 8601 week-numbering year and
week number.
## Examples
iex> q = TerminusDB.WOQL.iso_week("v:Date", "v:Year", "v:Week")
iex> q.op
:iso_week
"""
@spec iso_week(value(), value(), value()) :: t()
def iso_week(date, year, week) do
%__MODULE__{op: :iso_week, args: [date, year, week]}
end
@doc """
Builds a `MonthStartDate` — first day of the month for a given
`xsd:gYearMonth`.
## Examples
iex> q = TerminusDB.WOQL.month_start_date("v:YM", "v:Date")
iex> q.op
:month_start_date
"""
@spec month_start_date(value(), value()) :: t()
def month_start_date(year_month, date) do
%__MODULE__{op: :month_start_date, args: [year_month, date]}
end
@doc """
Builds a `MonthEndDate` — last day of the month for a given
`xsd:gYearMonth` (handles leap years).
## Examples
iex> q = TerminusDB.WOQL.month_end_date("v:YM", "v:Date")
iex> q.op
:month_end_date
"""
@spec month_end_date(value(), value()) :: t()
def month_end_date(year_month, date) do
%__MODULE__{op: :month_end_date, args: [year_month, date]}
end
@doc """
Builds a `MonthStartDates` — generator: every first-of-month date
in [start, end).
## Examples
iex> q = TerminusDB.WOQL.month_start_dates("v:Date", "v:Start", "v:End")
iex> q.op
:month_start_dates
"""
@spec month_start_dates(value(), value(), value()) :: t()
def month_start_dates(date, start_val, end_val) do
%__MODULE__{op: :month_start_dates, args: [date, start_val, end_val]}
end
@doc """
Builds a `MonthEndDates` — generator: every last-of-month date
in [start, end).
## Examples
iex> q = TerminusDB.WOQL.month_end_dates("v:Date", "v:Start", "v:End")
iex> q.op
:month_end_dates
"""
@spec month_end_dates(value(), value(), value()) :: t()
def month_end_dates(date, start_val, end_val) do
%__MODULE__{op: :month_end_dates, args: [date, start_val, end_val]}
end
@doc """
Builds an `InRange` — tests whether value falls within half-open
range [start, end).
## Examples
iex> q = TerminusDB.WOQL.in_range("v:Val", 10, 100)
iex> q.op
:in_range
"""
@spec in_range(value(), value(), value()) :: t()
def in_range(value, start_val, end_val) do
%__MODULE__{op: :in_range, args: [value, start_val, end_val]}
end
@doc """
Builds a `Sequence` — generates a sequence of values in half-open
[start, end) via backtracking. `step` and `count` are optional
(default `nil`).
## Examples
iex> q = TerminusDB.WOQL.sequence("v:V", 1, 10)
iex> q.op
:sequence
"""
@spec sequence(value(), value(), value(), value() | nil, value() | nil) :: t()
def sequence(value, start_val, end_val, step \\ nil, count \\ nil) do
%__MODULE__{op: :sequence, args: [value, start_val, end_val, step, count]}
end
@doc """
Builds a `RangeMin` — find minimum value in a list (any comparable
types).
## Examples
iex> q = TerminusDB.WOQL.range_min("v:List", "v:Min")
iex> q.op
:range_min
"""
@spec range_min(value(), value()) :: t()
def range_min(input_list, result) do
%__MODULE__{op: :range_min, args: [input_list, result]}
end
@doc """
Builds a `RangeMax` — find maximum value in a list (any comparable
types).
## Examples
iex> q = TerminusDB.WOQL.range_max("v:List", "v:Max")
iex> q.op
:range_max
"""
@spec range_max(value(), value()) :: t()
def range_max(input_list, result) do
%__MODULE__{op: :range_max, args: [input_list, result]}
end
# --------------------------------------------------------------------------
# CSV / IO
# --------------------------------------------------------------------------
@doc """
Builds a `Get` — reads a CSV/columns resource.
`as_vars` is a list of `Column` objects built by `woql_as/1`.
`query_resource` is built by `file/2`, `remote/2`, or `post/2`.
## Examples
iex> q = TerminusDB.WOQL.get(TerminusDB.WOQL.woql_as([{"name", "v:Name"}]), TerminusDB.WOQL.file("data.csv"))
iex> q.op
:get
"""
@spec get([map()], t()) :: t()
def get(as_vars, query_resource) do
%__MODULE__{op: :get, args: [as_vars, query_resource]}
end
@doc """
Builds a `Put` — writes an array of variables + optional column
names to a resource.
## Examples
iex> q = TerminusDB.WOQL.put(TerminusDB.WOQL.woql_as([{"name", "v:Name"}]), TerminusDB.WOQL.triple("v:S", "p", "v:O"), TerminusDB.WOQL.file("out.csv"))
iex> q.op
:put
"""
@spec put([map()], t(), t()) :: t()
def put(as_vars, query, query_resource) do
%__MODULE__{op: :put, args: [as_vars, query, query_resource]}
end
@doc """
Builds a list of `Column`/`Indicator` JSON-LD objects for use with
`get/2` and `put/3`.
Accepts a list of `{name_or_index, variable}` tuples.
## Examples
iex> cols = TerminusDB.WOQL.woql_as([{"name", "v:Name"}, {0, "v:Idx"}])
iex> length(cols)
2
iex> hd(cols)["@type"]
"Column"
"""
@spec woql_as([{String.t() | non_neg_integer(), woql_var()}]) :: [map()]
def woql_as(specs) when is_list(specs) do
Enum.map(specs, &build_as_column/1)
end
defp build_as_column({name, var}) when is_binary(name) do
var_name = if String.starts_with?(var, "v:"), do: String.slice(var, 2..-1//1), else: var
%{
"@type" => "Column",
"indicator" => %{"@type" => "Indicator", "name" => name},
"variable" => var_name
}
end
defp build_as_column({index, var}) when is_integer(index) do
var_name = if String.starts_with?(var, "v:"), do: String.slice(var, 2..-1//1), else: var
%{
"@type" => "Column",
"indicator" => %{"@type" => "Indicator", "index" => index},
"variable" => var_name
}
end
@doc """
Builds a `QueryResource` for a file source (CSV format by default).
## Examples
iex> q = TerminusDB.WOQL.file("data.csv")
iex> q.op
:file
"""
@spec file(String.t(), keyword()) :: t()
def file(fpath, opts \\ []) do
%__MODULE__{op: :file, args: [fpath, opts[:format] || "csv"]}
end
@doc """
Builds a `QueryResource` for a remote URL data source.
## Examples
iex> q = TerminusDB.WOQL.remote("https://example.com/data.csv")
iex> q.op
:remote
"""
@spec remote(String.t(), keyword()) :: t()
def remote(uri, opts \\ []) do
%__MODULE__{op: :remote, args: [uri, opts[:format] || "csv"]}
end
@doc """
Builds a `QueryResource` for a file posted as part of the request.
## Examples
iex> q = TerminusDB.WOQL.post("upload.csv")
iex> q.op
:post
"""
@spec post(String.t(), keyword()) :: t()
def post(fpath, opts \\ []) do
%__MODULE__{op: :post, args: [fpath, opts[:format] || "csv"]}
end
# --------------------------------------------------------------------------
# Serialization
# --------------------------------------------------------------------------
@doc """
Serializes a `WOQL.Query` to the JSON-LD wire format expected by the
`/api/woql` endpoint.
## Examples
iex> q = TerminusDB.WOQL.triple("v:S", "name", "v:N")
iex> jsonld = TerminusDB.WOQL.to_jsonld(q)
iex> jsonld["@type"]
"Triple"
"""
@spec to_jsonld(t()) :: map()
def to_jsonld(%__MODULE__{} = query) do
Encoder.encode(query)
end
@doc """
Deserializes a JSON-LD WOQL query back into a `WOQL.Query` struct.
## Examples
iex> jsonld = %{"@type" => "Triple", "subject" => %{"@type" => "NodeValue", "variable" => "S"}, "predicate" => %{"@type" => "NodeValue", "node" => "name"}, "object" => %{"@type" => "Value", "variable" => "N"}}
iex> q = TerminusDB.WOQL.from_jsonld(jsonld)
iex> q.op
:triple
"""
@spec from_jsonld(map()) :: t()
def from_jsonld(%{} = jsonld) do
Decoder.decode(jsonld)
end
# --------------------------------------------------------------------------
# Execution
# --------------------------------------------------------------------------
@doc """
Executes a WOQL query against the database scoped in `config`.
Returns `{:ok, result}` where `result` is a map containing `bindings` (a list
of maps, one per solution), or `{:error, TerminusDB.Error.t()}`.
## Options
- `:author` - commit author (for write queries).
- `:message` - commit message (for write queries).
- `:all_witnesses` - check for all errors (default `false`).
- `:organization` - overrides `config.organization`.
- `:repo` - overrides `config.repo`.
- `:branch` - overrides `config.branch`.
## Examples
iex> config = TerminusDB.Config.new(
...> endpoint: "http://localhost:6363",
...> adapter: fn req ->
...> {req, Req.Response.new(status: 200, body: %{"bindings" => [%{"Name" => "Alice"}]})}
...> end
...> ) |> TerminusDB.Config.with_database("mydb")
iex> q = TerminusDB.WOQL.select(["v:Name"],
...> TerminusDB.WOQL.and_([TerminusDB.WOQL.triple("v:P", "name", "v:Name")])
...> )
iex> {:ok, result} = TerminusDB.WOQL.execute(config, q)
iex> result["bindings"]
[%{"Name" => "Alice"}]
"""
@spec execute(Config.t(), t(), keyword()) :: {:ok, map()} | {:error, Error.t()}
def execute(config, %__MODULE__{} = query, opts \\ []) do
org = opts[:organization] || config.organization
case config.database do
nil ->
{:error, %Error{reason: :config, message: "no database scoped in config"}}
db ->
repo = opts[:repo] || config.repo
branch = opts[:branch] || config.branch
path = "woql/#{org}/#{db}/#{repo}/branch/#{branch}"
body =
%{"query" => to_jsonld(query)}
|> Params.maybe_put("commit_info", build_commit_info(opts))
|> Params.maybe_put("all_witnesses", opts[:all_witnesses])
Client.request(config, :post, path, json: body, area: :woql)
end
end
@doc """
Executes a WOQL query, or raises `TerminusDB.Error`.
## Examples
iex> config = TerminusDB.Config.new(
...> endpoint: "http://localhost:6363",
...> adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"bindings" => []})} end
...> ) |> TerminusDB.Config.with_database("mydb")
iex> q = TerminusDB.WOQL.triple("v:S", "p", "v:O")
iex> TerminusDB.WOQL.execute!(config, q)
%{"bindings" => []}
"""
@spec execute!(Config.t(), t(), keyword()) :: map()
def execute!(config, query, opts \\ []) do
case execute(config, query, opts) do
{:ok, body} -> body
{:error, error} -> raise error
end
end
@doc """
Executes a WOQL query and returns a lazy `Stream` of binding maps.
The stream uses the PrefaceRecord/Binding/PostscriptRecord protocol for
incremental delivery. Each element is a `%{"@type" => "Binding", ...}` map.
## Options
Same as `execute/3`.
## Examples
iex> config = TerminusDB.Config.new(
...> endpoint: "http://localhost:6363",
...> adapter: fn req -> {req, Req.Response.new(status: 200, body: [%{"@type" => "PrefaceRecord", "names" => ["Name"]}, %{"@type" => "Binding", "Name" => "Alice"}, %{"@type" => "PostscriptRecord"}])} end
...> ) |> TerminusDB.Config.with_database("mydb")
iex> q = TerminusDB.WOQL.select(["v:Name"], TerminusDB.WOQL.triple("v:P", "name", "v:Name"))
iex> {:ok, stream} = TerminusDB.WOQL.execute_stream(config, q)
iex> Enum.to_list(stream)
[%{"@type" => "Binding", "Name" => "Alice"}]
"""
@spec execute_stream(Config.t(), t(), keyword()) ::
{:ok, Enumerable.t()} | {:error, Error.t()}
def execute_stream(config, %__MODULE__{} = query, opts \\ []) do
org = opts[:organization] || config.organization
case config.database do
nil ->
{:error, %Error{reason: :config, message: "no database scoped in config"}}
db ->
repo = opts[:repo] || config.repo
branch = opts[:branch] || config.branch
path = "woql/#{org}/#{db}/#{repo}/branch/#{branch}"
body =
%{"query" => to_jsonld(query), "streaming" => true}
|> Params.maybe_put("commit_info", build_commit_info(opts))
|> Params.maybe_put("all_witnesses", opts[:all_witnesses])
case Client.request_response(config, :post, path,
json: body,
decode_body: false,
area: :woql
) do
{:ok, resp} ->
{:ok, woql_stream(resp)}
{:error, _} = error ->
error
end
end
end
defp woql_stream(resp) do
body = resp.body
if is_binary(body) do
body
|> String.split("\n", trim: true)
|> Stream.map(fn line ->
case Jason.decode(line) do
{:ok, decoded} -> decoded
{:error, reason} -> {:stream_decode_error, line, reason}
end
end)
|> Stream.reject(fn
{:stream_decode_error, _, _} -> false
%{"@type" => type} when type in ["PrefaceRecord", "PostscriptRecord"] -> true
_ -> false
end)
else
Stream.reject(body, &(&1["@type"] == "PrefaceRecord" or &1["@type"] == "PostscriptRecord"))
end
end
defp build_commit_info(opts) do
author = opts[:author]
message = opts[:message]
if author || message do
%{"author" => author || "", "message" => message || ""}
end
end
defp normalize_order_specs(specs) do
Enum.map(specs, &normalize_order_spec/1)
end
defp normalize_order_spec({var, order}) when is_binary(var) and is_atom(order) do
name = if String.starts_with?(var, "v:"), do: String.slice(var, 2..-1//1), else: var
{name, Atom.to_string(order)}
end
defp normalize_order_spec({key, order}) when is_atom(key) and is_atom(order) do
{Atom.to_string(key), Atom.to_string(order)}
end
end