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", " 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