Current section
Files
Jump to
Current section
Files
lib/aerospike/exp.ex
defmodule Aerospike.Exp do
@moduledoc """
Server-side expression builder.
Expressions are composable values used by Aerospike server features such as
filter expressions. Each builder returns an `%Aerospike.Exp{}` struct
containing pre-encoded expression wire bytes.
alias Aerospike.Exp
adult =
Exp.and_([
Exp.gte(Exp.int_bin("age"), Exp.val(18)),
Exp.lt(Exp.int_bin("age"), Exp.val(65))
])
active = Exp.eq(Exp.str_bin("status"), Exp.val("active"))
expression = Exp.and_([adult, active])
`val/1` maps Elixir values to literal expressions:
| Elixir term | Expression builder |
|-------------|--------------------|
| `integer()` | `int/1` |
| `float()` | `float/1` |
| `binary()` | `str/1` |
| `boolean()` | `bool/1` |
| `nil` | `nil_/0` |
Binaries passed to `val/1` or `str/1` are encoded as MessagePack strings.
Use `blob/1` when the expression value must use MessagePack binary format.
"""
import Kernel, except: [abs: 1, ceil: 1, floor: 1]
@enforce_keys [:wire]
defstruct [:wire]
@typedoc """
Opaque server-side expression.
The `wire` field contains encoded Aerospike expression bytes.
"""
@type t :: %__MODULE__{wire: binary()}
alias Aerospike.Protocol.Exp, as: Encoder
@particle_types %{
null: 0,
integer: 1,
float: 2,
string: 3,
blob: 4,
digest: 6,
bool: 17,
hll: 18,
map: 19,
list: 20,
ldt: 21,
geojson: 23
}
@regex_flags %{
none: 0,
extended: 1,
icase: 2,
nosub: 4,
newline: 8
}
@loop_var_parts %{
map_key: 0,
value: 1,
index: 2
}
@typedoc """
Aerospike expression result type tag.
Use this with typed bin, key, and loop-variable builders when the server must
know the expected expression value type.
"""
@type exp_type :: nil | :bool | :int | :string | :list | :map | :blob | :float | :geo | :hll
@typedoc """
Aerospike particle type name accepted by `particle_type/1`.
The returned integer can be compared with `bin_type/1`.
"""
@type particle_type ::
:null
| :integer
| :float
| :string
| :blob
| :digest
| :bool
| :hll
| :map
| :list
| :ldt
| :geojson
@typedoc """
Regular expression flag name accepted by `regex_flag/1` and `regex_flags/1`.
"""
@type regex_flag :: :none | :extended | :icase | :nosub | :newline
@typedoc """
Built-in loop-variable part used inside CDT filter expressions.
`:map_key` reads the current map key, `:value` reads the current list item or
map value, and `:index` reads the current collection index.
"""
@type loop_var_part :: :map_key | :value | :index
@doc """
Wraps pre-encoded expression bytes.
This is a low-level escape hatch for expressions built outside this module.
The binary is not validated as a complete Aerospike expression.
"""
@spec from_wire(binary()) :: t()
def from_wire(wire) when is_binary(wire), do: %__MODULE__{wire: wire}
@doc """
Encodes an expression's wire bytes as Base64.
Empty expressions return `{:error, :empty}` because Aerospike server APIs that
accept expressions require a non-empty expression payload.
"""
@spec base64(t()) :: {:ok, String.t()} | {:error, :empty}
def base64(%__MODULE__{wire: ""}), do: {:error, :empty}
def base64(%__MODULE__{wire: wire}) when is_binary(wire), do: {:ok, Base.encode64(wire)}
@doc "Integer literal expression."
@spec int(integer()) :: t()
def int(value) when is_integer(value), do: encode(%{val: value})
@doc "Float literal expression."
@spec float(float()) :: t()
def float(value) when is_float(value), do: encode(%{val: value})
@doc "String literal expression encoded as a MessagePack string."
@spec str(binary()) :: t()
def str(value) when is_binary(value), do: encode(%{val: {:string, value}})
@doc "Boolean literal expression."
@spec bool(boolean()) :: t()
def bool(value) when is_boolean(value), do: encode(%{val: value})
@doc "Nil literal expression."
@spec nil_() :: t()
def nil_, do: encode(%{val: nil})
@doc "Blob literal expression encoded as MessagePack binary data."
@spec blob(binary()) :: t()
def blob(value) when is_binary(value), do: encode(%{val: {:blob, value}})
@doc "GeoJSON literal expression."
@spec geo(binary()) :: t()
def geo(value) when is_binary(value), do: encode(%{val: {:geo, value}})
@doc "List literal expression."
@spec list(list()) :: t()
def list(values) when is_list(values), do: encode(%{val: {:list, values}})
@doc "Map literal expression."
@spec map(map()) :: t()
def map(values) when is_map(values), do: encode(%{val: {:map, values}})
@doc "Infinity value for CDT range expressions."
@spec infinity() :: t()
def infinity, do: encode(%{val: :infinity})
@doc "Wildcard value for CDT expressions."
@spec wildcard() :: t()
def wildcard, do: encode(%{val: :wildcard})
@doc """
Builds a literal expression from an Elixir value.
Binaries are treated as strings. Use `blob/1` explicitly for raw binary
semantics.
"""
@spec val(integer() | float() | binary() | boolean() | nil | list() | map()) :: t()
def val(value) when is_integer(value), do: int(value)
def val(value) when is_float(value), do: float(value)
def val(value) when is_binary(value), do: str(value)
def val(value) when is_boolean(value), do: bool(value)
def val(nil), do: nil_()
def val(value) when is_list(value), do: list(value)
def val(value) when is_map(value), do: map(value)
@doc "Record key expression of the specified expression type."
@spec key(exp_type()) :: t()
def key(type), do: encode(%{cmd: :key, type: type})
@doc "Reads an integer bin from the current record."
@spec int_bin(String.t()) :: t()
def int_bin(name) when is_binary(name), do: bin(name, :int)
@doc "Reads a float bin from the current record."
@spec float_bin(String.t()) :: t()
def float_bin(name) when is_binary(name), do: bin(name, :float)
@doc "Reads a string bin from the current record."
@spec str_bin(String.t()) :: t()
def str_bin(name) when is_binary(name), do: bin(name, :string)
@doc "Reads a boolean bin from the current record."
@spec bool_bin(String.t()) :: t()
def bool_bin(name) when is_binary(name), do: bin(name, :bool)
@doc "Reads a blob bin from the current record."
@spec blob_bin(String.t()) :: t()
def blob_bin(name) when is_binary(name), do: bin(name, :blob)
@doc "Reads a geospatial bin from the current record."
@spec geo_bin(String.t()) :: t()
def geo_bin(name) when is_binary(name), do: bin(name, :geo)
@doc "Reads a list bin from the current record."
@spec list_bin(String.t()) :: t()
def list_bin(name) when is_binary(name), do: bin(name, :list)
@doc "Reads a map bin from the current record."
@spec map_bin(String.t()) :: t()
def map_bin(name) when is_binary(name), do: bin(name, :map)
@doc "Reads an HLL bin from the current record."
@spec hll_bin(String.t()) :: t()
def hll_bin(name) when is_binary(name), do: bin(name, :hll)
@doc "True when the named bin exists in the current record."
@spec bin_exists(String.t()) :: t()
def bin_exists(name) when is_binary(name), do: ne(bin_type(name), int(particle_type(:null)))
@doc "Reads the named bin's integer particle type."
@spec bin_type(String.t()) :: t()
def bin_type(name) when is_binary(name), do: encode(%{cmd: :bin_type, val: name})
@doc "Record time-to-live in seconds."
@spec ttl() :: t()
def ttl, do: encode(%{cmd: :ttl})
@doc "Record expiration time as an absolute server timestamp."
@spec void_time() :: t()
def void_time, do: encode(%{cmd: :void_time})
@doc "Record last-update timestamp."
@spec last_update() :: t()
def last_update, do: encode(%{cmd: :last_update})
@doc "Milliseconds since the record was last updated."
@spec since_update() :: t()
def since_update, do: encode(%{cmd: :since_update})
@doc "True when the record has a stored user key."
@spec key_exists() :: t()
def key_exists, do: encode(%{cmd: :key_exists})
@doc "Record set name."
@spec set_name() :: t()
def set_name, do: encode(%{cmd: :set_name})
@doc "True when the record is a tombstone."
@spec tombstone?() :: t()
def tombstone?, do: encode(%{cmd: :is_tombstone})
@doc "Record size in bytes on storage device."
@spec record_size() :: t()
def record_size, do: encode(%{cmd: :record_size})
@doc "Record digest modulo expression."
@spec digest_modulo(integer()) :: t()
def digest_modulo(value) when is_integer(value), do: encode(%{cmd: :digest_modulo, val: value})
@doc "Integer particle type value returned by `bin_type/1`."
@spec particle_type(particle_type()) :: non_neg_integer()
def particle_type(name) when is_map_key(@particle_types, name),
do: Map.fetch!(@particle_types, name)
@doc "Integer regular expression flag value for `regex_compare/3`."
@spec regex_flag(regex_flag()) :: non_neg_integer()
def regex_flag(name) when is_map_key(@regex_flags, name), do: Map.fetch!(@regex_flags, name)
@doc "Combines regular expression flags for `regex_compare/3`."
@spec regex_flags([regex_flag()]) :: non_neg_integer()
def regex_flags(flags) when is_list(flags) do
Enum.reduce(flags, 0, fn flag, acc -> Bitwise.bor(acc, regex_flag(flag)) end)
end
@doc "Integer loop-variable part value for typed loop-variable builders."
@spec loop_var_part(loop_var_part()) :: non_neg_integer()
def loop_var_part(name) when is_map_key(@loop_var_parts, name),
do: Map.fetch!(@loop_var_parts, name)
@doc "Regular expression comparison against a string expression."
@spec regex_compare(String.t(), non_neg_integer(), t()) :: t()
def regex_compare(regex, flags, %__MODULE__{} = expression)
when is_binary(regex) and is_integer(flags) and flags >= 0 do
encode(%{cmd: :regex, val: {regex, flags}, exps: expression_nodes([expression])})
end
@doc "Geospatial comparison."
@spec geo_compare(t(), t()) :: t()
def geo_compare(%__MODULE__{} = left, %__MODULE__{} = right) do
encode(%{cmd: :geo_compare, exps: expression_nodes([left, right])})
end
@doc "Equal comparison."
@spec eq(t(), t()) :: t()
def eq(%__MODULE__{} = left, %__MODULE__{} = right), do: compare(:eq, left, right)
@doc "Not-equal comparison."
@spec ne(t(), t()) :: t()
def ne(%__MODULE__{} = left, %__MODULE__{} = right), do: compare(:ne, left, right)
@doc "Greater-than comparison."
@spec gt(t(), t()) :: t()
def gt(%__MODULE__{} = left, %__MODULE__{} = right), do: compare(:gt, left, right)
@doc "Greater-than-or-equal comparison."
@spec gte(t(), t()) :: t()
def gte(%__MODULE__{} = left, %__MODULE__{} = right), do: compare(:gte, left, right)
@doc "Less-than comparison."
@spec lt(t(), t()) :: t()
def lt(%__MODULE__{} = left, %__MODULE__{} = right), do: compare(:lt, left, right)
@doc "Less-than-or-equal comparison."
@spec lte(t(), t()) :: t()
def lte(%__MODULE__{} = left, %__MODULE__{} = right), do: compare(:lte, left, right)
@doc """
Logical AND over two or more expressions.
The function name has a trailing underscore because `and` is an Elixir
reserved word.
"""
@spec and_([t(), ...]) :: t()
def and_(expressions) when is_list(expressions) and length(expressions) >= 2 do
encode(%{cmd: :and_, exps: expression_nodes(expressions)})
end
@doc """
Logical OR over two or more expressions.
The function name has a trailing underscore because `or` is an Elixir
reserved word.
"""
@spec or_([t(), ...]) :: t()
def or_(expressions) when is_list(expressions) and length(expressions) >= 2 do
encode(%{cmd: :or_, exps: expression_nodes(expressions)})
end
@doc """
Logical NOT of an expression.
The function name has a trailing underscore because `not` is an Elixir
reserved word.
"""
@spec not_(t()) :: t()
def not_(%__MODULE__{} = expression) do
encode(%{cmd: :not_, exps: expression_nodes([expression])})
end
@doc "Logical exclusive-or over two or more expressions."
@spec exclusive([t(), ...]) :: t()
def exclusive(expressions) when is_list(expressions) and length(expressions) >= 2 do
encode(%{cmd: :exclusive, exps: expression_nodes(expressions)})
end
@doc "Numeric addition over one or more expressions."
@spec add([t(), ...]) :: t()
def add(expressions), do: variadic(:add, expressions)
@doc "Numeric subtraction over one or more expressions."
@spec sub([t(), ...]) :: t()
def sub(expressions), do: variadic(:sub, expressions)
@doc "Numeric multiplication over one or more expressions."
@spec mul([t(), ...]) :: t()
def mul(expressions), do: variadic(:mul, expressions)
@doc "Numeric division over one or more expressions."
@spec div_([t(), ...]) :: t()
def div_(expressions), do: variadic(:div_, expressions)
@doc "Numeric power expression."
@spec pow(t(), t()) :: t()
def pow(%__MODULE__{} = base, %__MODULE__{} = exponent), do: compare(:pow, base, exponent)
@doc "Numeric logarithm expression."
@spec log(t(), t()) :: t()
def log(%__MODULE__{} = number, %__MODULE__{} = base), do: compare(:log, number, base)
@doc "Integer modulo expression."
@spec mod(t(), t()) :: t()
def mod(%__MODULE__{} = numerator, %__MODULE__{} = denominator),
do: compare(:mod, numerator, denominator)
@doc "Absolute value expression."
@spec abs(t()) :: t()
def abs(%__MODULE__{} = expression), do: unary(:abs, expression)
@doc "Floor expression."
@spec floor(t()) :: t()
def floor(%__MODULE__{} = expression), do: unary(:floor, expression)
@doc "Ceiling expression."
@spec ceil(t()) :: t()
def ceil(%__MODULE__{} = expression), do: unary(:ceil, expression)
@doc "Converts a numeric expression to an integer."
@spec to_int(t()) :: t()
def to_int(%__MODULE__{} = expression), do: unary(:to_int, expression)
@doc "Converts a numeric expression to a float."
@spec to_float(t()) :: t()
def to_float(%__MODULE__{} = expression), do: unary(:to_float, expression)
@doc "Integer bitwise AND over two or more expressions."
@spec int_and([t(), ...]) :: t()
def int_and(expressions) when is_list(expressions) and length(expressions) >= 2,
do: variadic(:int_and, expressions)
@doc "Integer bitwise OR over two or more expressions."
@spec int_or([t(), ...]) :: t()
def int_or(expressions) when is_list(expressions) and length(expressions) >= 2,
do: variadic(:int_or, expressions)
@doc "Integer bitwise XOR over two or more expressions."
@spec int_xor([t(), ...]) :: t()
def int_xor(expressions) when is_list(expressions) and length(expressions) >= 2,
do: variadic(:int_xor, expressions)
@doc "Integer bitwise NOT expression."
@spec int_not(t()) :: t()
def int_not(%__MODULE__{} = expression), do: unary(:int_not, expression)
@doc "Integer left-shift expression."
@spec int_lshift(t(), t()) :: t()
def int_lshift(%__MODULE__{} = value, %__MODULE__{} = shift),
do: compare(:int_lshift, value, shift)
@doc "Integer logical right-shift expression."
@spec int_rshift(t(), t()) :: t()
def int_rshift(%__MODULE__{} = value, %__MODULE__{} = shift),
do: compare(:int_rshift, value, shift)
@doc "Integer arithmetic right-shift expression."
@spec int_arshift(t(), t()) :: t()
def int_arshift(%__MODULE__{} = value, %__MODULE__{} = shift),
do: compare(:int_arshift, value, shift)
@doc "Count of set bits in an integer expression."
@spec int_count(t()) :: t()
def int_count(%__MODULE__{} = expression), do: unary(:int_count, expression)
@doc "Scan integer bits from left to right for a search bit."
@spec int_lscan(t(), t()) :: t()
def int_lscan(%__MODULE__{} = value, %__MODULE__{} = search),
do: compare(:int_lscan, value, search)
@doc "Scan integer bits from right to left for a search bit."
@spec int_rscan(t(), t()) :: t()
def int_rscan(%__MODULE__{} = value, %__MODULE__{} = search),
do: compare(:int_rscan, value, search)
@doc "Minimum value over one or more expressions."
@spec min([t(), ...]) :: t()
def min(expressions), do: variadic(:min, expressions)
@doc "Maximum value over one or more expressions."
@spec max([t(), ...]) :: t()
def max(expressions), do: variadic(:max, expressions)
@doc "Conditionally selects an action expression."
@spec cond_([t(), ...]) :: t()
def cond_(expressions) when is_list(expressions) and length(expressions) >= 3 do
encode(%{cmd: :cond, exps: expression_nodes(expressions)})
end
@doc "Defines variables and evaluates a scoped expression."
@spec let([t(), ...]) :: t()
def let(expressions) when is_list(expressions) and length(expressions) >= 2 do
encode(%{cmd: :let, exps: expression_nodes(expressions)})
end
@doc "Assigns a variable for use inside `let/1`."
@spec def_(String.t(), t()) :: t()
def def_(name, %__MODULE__{} = expression) when is_binary(name) do
encode(%{cmd: :def, val: name, exps: expression_nodes([expression])})
end
@doc "Reads a variable defined by `let/1`."
@spec var(String.t()) :: t()
def var(name) when is_binary(name), do: encode(%{cmd: :var, val: name})
@doc "Reads a built-in loop variable with the specified expression type."
@spec loop_var(exp_type(), loop_var_part()) :: t()
def loop_var(type, part), do: encode(%{cmd: :loop_var, type: type, val: loop_var_part(part)})
@doc "Reads a nil built-in loop variable."
@spec nil_loop_var(loop_var_part()) :: t()
def nil_loop_var(part), do: loop_var(nil, part)
@doc "Reads a boolean built-in loop variable."
@spec bool_loop_var(loop_var_part()) :: t()
def bool_loop_var(part), do: loop_var(:bool, part)
@doc "Reads an integer built-in loop variable."
@spec int_loop_var(loop_var_part()) :: t()
def int_loop_var(part), do: loop_var(:int, part)
@doc "Reads a float built-in loop variable."
@spec float_loop_var(loop_var_part()) :: t()
def float_loop_var(part), do: loop_var(:float, part)
@doc "Reads a string built-in loop variable."
@spec str_loop_var(loop_var_part()) :: t()
def str_loop_var(part), do: loop_var(:string, part)
@doc "Reads a blob built-in loop variable."
@spec blob_loop_var(loop_var_part()) :: t()
def blob_loop_var(part), do: loop_var(:blob, part)
@doc "Reads a list built-in loop variable."
@spec list_loop_var(loop_var_part()) :: t()
def list_loop_var(part), do: loop_var(:list, part)
@doc "Reads a map built-in loop variable."
@spec map_loop_var(loop_var_part()) :: t()
def map_loop_var(part), do: loop_var(:map, part)
@doc "Reads a geospatial built-in loop variable."
@spec geo_loop_var(loop_var_part()) :: t()
def geo_loop_var(part), do: loop_var(:geo, part)
@doc "Reads an HLL built-in loop variable."
@spec hll_loop_var(loop_var_part()) :: t()
def hll_loop_var(part), do: loop_var(:hll, part)
@doc "Unknown expression value."
@spec unknown() :: t()
def unknown, do: encode(%{cmd: :unknown})
@doc "Result-remove expression value."
@spec remove_result() :: t()
def remove_result, do: encode(%{cmd: :remove_result})
defp bin(name, type), do: encode(%{cmd: :bin, val: name, type: type})
defp unary(operator, %__MODULE__{} = expression) do
encode(%{cmd: operator, exps: expression_nodes([expression])})
end
defp variadic(operator, [_ | _] = expressions) do
encode(%{cmd: operator, exps: expression_nodes(expressions)})
end
defp compare(operator, %__MODULE__{} = left, %__MODULE__{} = right) do
encode(%{cmd: operator, exps: expression_nodes([left, right])})
end
defp expression_nodes(expressions) do
Enum.map(expressions, fn %__MODULE__{wire: wire} -> %{bytes: wire} end)
end
defp encode(node), do: %__MODULE__{wire: Encoder.encode(node)}
end