TypeDB.ConceptRow (TypeDB v0.5.1)

Copy Markdown View Source

One row of a conceptRows answer: a map from variable name to concept.

Rows implement Access, so a variable can be read directly:

row["person"]
#=> %TypeDB.Concept.Entity{iid: "0x1e0...", type: %TypeDB.Concept.EntityType{label: "person"}}

get_in(row, ["name"])
#=> %TypeDB.Concept.Attribute{value: "Alice", value_type: "string"}

A variable bound to nothing yields nil, which is indistinguishable from an absent variable through Access. Use fetch/2 when the difference matters.

involved_blocks is populated only when the query ran with include_query_structure: true; it lists the ids of the query blocks that produced this row.

Summary

Functions

Fetches a variable, distinguishing "bound to nothing" from "not present".

Returns a variable's concept, or default when it is absent or unbound.

Returns the row as a plain map of variable name to wire value.

Builds a struct out of the row, matching variable names to field names.

Returns the row as a plain map of variable name to native Elixir term.

Returns the natively typed value of a variable. See TypeDB.Concept.typed_value/1.

Returns the wire value of a variable bound to an attribute or value.

Returns the variable names bound in this row.

Types

t()

@type t() :: %TypeDB.ConceptRow{
  data: %{optional(String.t()) => TypeDB.Concept.entry()},
  involved_blocks: [non_neg_integer()] | nil
}

Functions

fetch(concept_row, variable)

@spec fetch(t(), String.t()) :: {:ok, TypeDB.Concept.entry()} | :error

Fetches a variable, distinguishing "bound to nothing" from "not present".

{:ok, nil}   # the variable exists but matched nothing
:error       # the variable is not in this row

get(concept_row, variable, default \\ nil)

@spec get(t(), String.t(), term()) :: TypeDB.Concept.entry() | term()

Returns a variable's concept, or default when it is absent or unbound.

to_map(concept_row)

@spec to_map(t()) :: %{optional(String.t()) => term()}

Returns the row as a plain map of variable name to wire value.

The keys are strings — the query's own variable names, exactly as TypeDB sent them back.

That matters if you are reaching for Kernel.struct/2, which ignores keys that are not atoms naming a field: handed a string-keyed map it returns the struct's defaults and reports nothing, so the conversion silently produces a struct full of nils. Use to_struct/3 instead.

The values are TypeDB's own, exactly as they arrived — a decimal is the string "12.345dec", TypeQL's literal suffix and all, and a duration the string "P1Y2M3DT4H5M6S". to_typed_map/1 is the same map with every value cast to a native Elixir term.

to_struct(row, module, opts \\ [])

@spec to_struct(t(), module(), keyword()) :: struct()

Builds a struct out of the row, matching variable names to field names.

Kernel.struct/2 cannot do this: handed a string-keyed map it ignores every key and hands back the struct's defaults, so a mistyped variable produces a struct full of nils and no complaint. This raises instead, naming the variable and listing the fields that do exist.

defmodule Person do
  defstruct [:name, :age]
end

TypeDB.query!(conn, "social", """
  match $p isa person, has name $name, has age $age;
  select $name, $age;
""")
|> Enum.map(&TypeDB.ConceptRow.to_struct(&1, Person))
#=> [%Person{name: "Alice", age: 31}]

The select stage is not decoration. A match binds every variable it names, $p included, and $p names no field of Person — so without it this raises. Narrowing the row is the query's job, and TypeQL has a stage for it.

Values are unwrapped as to_map/1 unwraps them — TypeDB's own, uncast. Pass typed: true for to_typed_map/1's conversions instead, which is what you want if a field holds a duration, a decimal or a timestamp:

TypeDB.ConceptRow.to_struct(row, Shift, typed: true)
#=> %Shift{worked: %TypeDB.Duration{months: 14, days: 3, nanos: 14706000000000, raw: "P1Y2M3DT4H5M6S"}}

A field the query did not select keeps the struct's own default, since selecting a subset of the fields is an ordinary thing to want; a variable with no matching field is a mistake, and is reported as one.

Examples

iex> row = %TypeDB.ConceptRow{data: %{"scheme" => nil, "host" => nil}}
iex> TypeDB.ConceptRow.to_struct(row, URI)
%URI{scheme: nil, host: nil}

iex> row = %TypeDB.ConceptRow{data: %{"hsot" => nil}}
iex> TypeDB.ConceptRow.to_struct(row, URI)
** (ArgumentError) query variable "hsot" does not name a field of URI. Its fields are: :authority, :fragment, :host, :path, :port, :query, :scheme, :userinfo. Narrow the row with TypeQL's select stage, or add the field.

to_typed_map(concept_row)

@spec to_typed_map(t()) :: %{optional(String.t()) => term()}

Returns the row as a plain map of variable name to native Elixir term.

to_map/1 with typed_value/2's conversions applied to every value, so a whole row can be read natively in one call rather than a variable at a time: a datetime arrives as a NaiveDateTime, a duration as a TypeDB.Duration, a decimal as a Decimal when that optional dependency is loaded. See TypeDB.Concept.typed_value/1 for the full table.

Concepts that are not attributes or values — an entity, a relation, a type — are left alone, exactly as to_map/1 leaves them.

TypeDB.ConceptRow.to_map(row)
#=> %{"name" => "Alice", "worked" => "P1Y2M3DT4H5M6S"}

TypeDB.ConceptRow.to_typed_map(row)
#=> %{"name" => "Alice", "worked" => %TypeDB.Duration{months: 14, days: 3, nanos: 14706000000000, raw: "P1Y2M3DT4H5M6S"}}

typed_value(row, variable)

@spec typed_value(t(), String.t()) :: term()

Returns the natively typed value of a variable. See TypeDB.Concept.typed_value/1.

value(row, variable)

@spec value(t(), String.t()) :: term()

Returns the wire value of a variable bound to an attribute or value.

variables(concept_row)

@spec variables(t()) :: [String.t()]

Returns the variable names bound in this row.