Skip to main content

Built-in Functions in LoraDB

Functions are expressions. You can use them anywhere an expression is accepted: in RETURN, WITH, WHERE, ORDER BY, SET, map projections, list comprehensions, and nested inside other function calls.

MATCH (d:Doc)
WITH d,
   vector.similarity(d.embedding, $query) AS score,
   temporal.truncate('month', d.published_at) AS month
WHERE score >= 0.75
RETURN d.title, month, score
ORDER BY score DESC
LIMIT 10

Function names are case-insensitive. Canonical LoraDB functions are mostly namespaced, such as string.lower, math.sqrt, list.range, temporal.now, geo.distance, vector.similarity, type.of, and cast.try. A small set of historical and convenience aliases remains for common Cypher-style helpers and zero-argument value helpers, such as toInteger, toString, size, head, length, coalesce, and now.

The analyzer resolves every function call before execution. That means unknown names, wrong argument counts, unknown type literals, and invalid enum literals such as vector metric names are rejected before rows are scanned. The executor then evaluates only validated calls.

Most functions propagate null — any null argument makes the result null. The exceptions are aggregates, coalesce, current-time helpers, and constants/random helpers.

Function expression basics

Functions do not introduce a new query clause. They are part of the expression language, so they evaluate once for each row flowing through the clause that contains them.

MATCH (u:User)
WITH u, string.lower(u.email) AS email
WHERE email ENDS WITH '@loradb.com'
RETURN u.name, email
ORDER BY email

In that query:

  • string.lower(u.email) runs for each matched User row.
  • WITH gives the result a name so later clauses can reuse it.
  • WHERE, RETURN, and ORDER BY all see the shaped row from WITH.

Functions are side-effect free. They cannot create nodes, update properties, query indexes by themselves, or change the graph. Graph writes still happen through clauses such as CREATE, MERGE, SET, and DELETE; functions only compute values for those clauses to use.

Reading signatures and examples

Function pages use compact signatures:

NotationMeaning
fn(x)one required argument
fn(x[, y])y is optional
fn(a, b, ...)accepts a variable number of arguments
LIST<T>a list whose elements are usually the same type
null → nulla null input returns null instead of an error

Examples are written as complete Cypher fragments whenever possible. Comments show representative results, not a promise about exact display format in every host binding.

RETURN string.slice('loradb', 0, 4);     // 'lora'
RETURN '2024-01-15'::DATE;               // DATE value
RETURN TRY_CAST('not a date' AS DATE)   // null

How functions are organised

LoraDB keeps value construction and value operations separate:

Use casePreferred syntaxWhy
Build a typed value in query text'2024-01-15'::DATE, {x: 1, y: 2}::POINT, [1,2,3]::VECTOR<INTEGER>(3)Casts make the target type explicit.
Convert with CAST syntaxCAST(value AS TYPE)First-class Cypher grammar for strict conversion.
Convert without throwingTRY_CAST(value AS TYPE)Nullable cast; returns null when conversion fails.
Ask about a value's typetype.of(value)Returns tags such as "DATE" or "VECTOR<FLOAT32>(384)".
Work with temporal valuestemporal.now(), temporal.truncate('month', d)Current-time helpers and temporal operations.
Work with pointsgeo.distance(a, b), geo.within_bbox(p, ll, ur)Spatial operations over existing POINT values.
Work with vectorsvector.similarity(a, b), vector.distance(a, b, EUCLIDEAN)Vector math over existing VECTOR values.

The important distinction: temporal.*, geo.*, and vector.* are operation namespaces. They are not the default way to construct DATE, POINT, or VECTOR values in query text.

RETURN '2024-01-15'::DATE,
     CAST('2024-01-15' AS DATE),
     TRY_CAST($maybe_date AS DATE)

Use host-language helpers separately when binding parameters from Node, Python, Go, Ruby, WASM, or Rust. For example, a Node query may bind { at: datetime("2026-05-01T09:00:00Z") }, but the equivalent query-text literal is '2026-05-01T09:00:00Z'::DATETIME.

Choosing a construction form

Use the form that best matches where the value comes from:

Source valueRecommended query syntaxNotes
Literal ISO date/time text'2026-05-01'::DATEMost readable for handwritten queries.
Literal map point{longitude: 4.89, latitude: 52.37}::POINTLets the POINT cast validate CRS/SRID rules.
Literal numeric vector[0.1, 0.2, 0.3]::VECTOR<FLOAT32>(3)Dimension and coordinate type are visible at the use site.
Parameter that may be invalidTRY_CAST($value AS DATE)Returns null, so pair it with IS NOT NULL or coalesce.
Generated query textCAST($value AS DATE)Strict conversion using the Cypher grammar.

Strict casts are useful when bad input should stop the query. TRY_CAST is better for ingestion and optional filters, where a failed conversion should become null and be handled by normal Cypher predicates.

The older constructor-shaped temporal, spatial, and vector forms are not the preferred public query syntax. Use casts in query text, and use client-library binding helpers only when creating parameters in a host language.

Calling functions

A function call has three parts: the name, the arguments, and sometimes a literal type or option argument.

RETURN string.lower('LORA') AS name,
     math.round(3.14159, 2) AS rounded,
     vector.distance($a, $b, EUCLIDEAN) AS distance,
     TRY_CAST($raw AS DATE) AS maybe_date

Namespaced functions are ordinary expression functions. They do not require CALL, do not stream rows by themselves, and can be nested inside other expressions:

MATCH (d:Doc)
WHERE type.of(d.published_at) = 'DATE'
RETURN string.upper(coalesce(d.title, 'untitled')) AS title,
     temporal.truncate('year', d.published_at) AS year

Some arguments are not values from the graph; they are compile-time symbols:

Symbol kindUsed byExample
Type literalcasts and type checksTRY_CAST($x AS INTEGER), type.is($x, DATE)
Vector metricvector distance and normvector.distance(a, b, EUCLIDEAN), vector.norm(v, MANHATTAN)

Vector metrics can also be supplied as strings when that is more convenient for generated queries. If the target type itself must be dynamic, use the lower-level cast.to / cast.try helpers with a type string:

RETURN cast.try($raw, $type_name) AS maybe_date,
     vector.distance($a, $b, 'euclidean') AS distance

Namespaces and aliases

Canonical names are deliberately explicit:

NamespaceContainsExamples
string.*string case, search, tokenizing, slicing, regex, encodingstring.lower(x), string.words(x), string.count(x, needle)
math.*arithmetic helpers, trig, constants, randommath.abs(x), math.hypot(a, b), math.random()
number.* / bits.*numeric formatting, base conversion, numeric predicates, bit operationsnumber.format(n, 2), number.to_base(n, 16), bits.and(a, b)
list.*list construction, indexing, and transformslist.range(1, 10), list.at(xs, -1), list.take_last(xs, 3)
map.*map lookup, patching, projection, deep merge, nested paths, entries, flatteningmap.get(m, 'k'), map.get_path(m, 'a.b'), map.deep_merge(a, b)
temporal.*current time, truncation, differences, fieldstemporal.now(), temporal.today(), temporal.between(a, b)
geo.*spatial predicates and distancesgeo.distance(a, b), geo.within_bbox(p, ll, ur)
vector.*similarity, distance, norms, vector shapevector.similarity(a, b), vector.dimension(v)
text.*string distance, similarity, and phonetic helperstext.distance(a, b, 'levenshtein'), text.phonetic(s, 'soundex')
bytes.* / crypto.*byte conversion, compression, and hashesbytes.base64_encode(x), crypto.blake3(x)
uuid.* / json.*UUID generation/validation and JSON encoding/path accessuuid.new(), json.path(x, '$.a[0]')
path.*path decomposition and path lengthpath.nodes(p), path.edges(p), path.length(p)
node.* / edge.*node and relationship introspectionnode.labels(n), edge.type(r)
type.*runtime type inspection and checkstype.of(x), type.is(x, DATE)
cast.*explicit conversion helperscast.to(x, DATE), cast.try(x, INTEGER)
value.*polymorphic value helpersvalue.size(x), value.keys(x), value.coalesce(a, b)

Compatibility aliases exist for common Cypher spellings:

AliasCanonical behavior
toString(x)x::STRING / CAST(x AS STRING)
toInteger(x)x::INTEGER / CAST(x AS INTEGER)
toIntegerOrNull(x)TRY_CAST(x AS INTEGER)
size(x)value.size(x)
head(xs) / last(xs)list first / last helpers
id(x), labels(n), type(r)entity introspection helpers
now()temporal.now()
timestamp()temporal.timestamp()
timezone()temporal.timezone()
new()uuid.new()
random()math.random()

Prefer canonical names in new documentation when the namespace clarifies intent. Use aliases where they are the familiar Cypher form and already documented on the category page.

Category guide

The function library is intentionally small. Each category covers one kind of value work:

CategoryUse it forGood first function
AggregationCollapsing many rows into grouped summariescount(*)
StringCleaning, slicing, parsing, and comparing textstring.trim, string.lower
MathNumeric formulas and random/constantsmath.round, math.sqrt
NumberFormatting, radix conversion, integer predicates, and bit helpersnumber.format, number.to_base
ListSelecting, reshaping, and reducing list valuesvalue.size, list.range
TemporalCurrent time, calendar math, truncation, date differencestemporal.now()
SpatialPoint distance, bounding boxes, CRS-aware component accessgeo.distance
VectorEmbedding similarity, distance metrics, vector introspectionvector.similarity
Entity/valueInspecting graph values, maps, and runtime typesvalue.properties, type.of

If a task needs graph traversal, start with a pattern in MATCH. If it needs row grouping, start with aggregation. If it needs to reshape a single value already in the row, reach for a scalar function.

// Traversal first, then scalar functions, then aggregation.
MATCH (u:User)-[:POSTED]->(p:Post)
WITH u, temporal.truncate('month', p.created_at) AS month
RETURN u.id, month, count(*) AS posts
ORDER BY month

Errors and nulls

For a single expression, LoraDB distinguishes validation failures, runtime conversion failures, ordinary null propagation, and aggregate semantics.

Validation errors

These are rejected while the query is analyzed:

  • unknown function names
  • wrong argument counts
  • invalid type names in CAST, TRY_CAST, cast.to, or cast.try
  • invalid vector metrics in vector.distance or vector.norm

For example, vector.distance(a, b) is invalid because distance needs a metric, and vector.distance(a, b, BOGUS) is invalid because BOGUS is not a supported metric.

Conversion errors

SituationResult
Unknown function or wrong argument countanalysis error before execution
Invalid strict conversion, such as 'bad'::DATEruntime error
Nullable conversion, such as TRY_CAST('bad' AS DATE)null

Strict casts are for inputs that must be valid. Nullable casts are for ingestion, optional filters, and user-supplied values where invalid data should simply disappear from the result:

UNWIND $rows AS row
WITH row, TRY_CAST(row.signup_date AS DATE) AS signup_date
WHERE signup_date IS NOT NULL
CREATE (:Signup {email: row.email, signup_date: signup_date})

Null propagation

Most non-aggregate functions follow normal null propagation:

RETURN geo.distance(null, {x: 1, y: 2}::POINT),       // null
     vector.similarity(null, [1, 2, 3]::VECTOR<INTEGER>(3)), // null
     temporal.truncate('month', null),              // null
     coalesce(null, 'fallback')                     // 'fallback'

That behavior is intentional: missing input usually means missing output, and the query can decide how to handle it with IS NULL, IS NOT NULL, coalesce, or CASE.

Aggregate behavior

Aggregates are different because they operate over groups of rows: count(*) counts rows, count(x) counts non-null x, and functions such as sum, avg, min, and max skip null inputs.

MATCH (p:Person)
RETURN count(*) AS people,
     count(p.email) AS people_with_email,
     avg(p.age) AS average_known_age

Filtering after nullable functions

null is not equal to anything, including itself. When you use TRY_CAST or a function that can return null, filter with IS NULL or IS NOT NULL.

UNWIND $rows AS row
WITH row, TRY_CAST(row.started_at AS DATETIME) AS started_at
WHERE started_at IS NOT NULL
CREATE (:Event {id: row.id, started_at: started_at})

For default values, use coalesce:

RETURN coalesce(TRY_CAST($limit AS INTEGER), 100) AS limit

Categories

CategoryExamplesReference
Aggregationcount, sum, collect, percentileContAggregation
Stringstring.lower, string.words, string.count, string.normalizeString
Mathmath.abs, math.hypot, math.log_base, math.randomMath
Numbernumber.format, number.to_base, number.from_base, bits.andNumber
Listvalue.size, list.at, list.take_last, reduceList
Mapmap.get, map.get_path, map.pick, map.deep_merge, map.entriesMap
Temporaltemporal.now, temporal.today, temporal.between, '2024-01-01'::DATETemporal
Spatial{x: 1, y: 2}::POINT, geo.distanceSpatial
Vector[1,2,3]::VECTOR<INTEGER>(3), vector.similarity, vector.distance, vector.norm, vector.dimension, vector.coordinatesVector
Utilitytype.of, json.path, text.distance, bytes.base64_encode, uuid.newUtility
Pathpath.length, path.nodes, path.edgesPaths

The category pages are the detailed references. This page explains the shared rules that apply across categories.

Entity introspection

Common aliasCanonical helperTakesReturns
id(x)value.id(x)node | relationshipInt — internal id
labels(n)node.labels(n)nodeList<String>
type(r)edge.type(r)relationshipString — rel type
keys(x)value.keys(x)node | rel | mapList<String>
properties(x)value.properties(x)node | rel | mapMap
MATCH (u:User)-[r:FOLLOWS]->(v:User)
RETURN id(u), labels(u), type(r), keys(u), properties(u)

Common uses

// Dump every property on a node as a map
MATCH (u:User {id: $id}) RETURN properties(u)

;// Discover which labels a node carries
MATCH (n) WHERE id(n) = $raw_id RETURN labels(n)

;// Inspect the type of a matched edge
MATCH (a)-[r]->(b) RETURN type(r), count(*) ORDER BY count(*) DESC

;// Avoid duplicate pair rows
MATCH (a)-[:KNOWS]-(b) WHERE id(a) < id(b) RETURN a, b

See Graph model → Identity for why id() is opaque.

Use aliases when writing familiar Cypher-style queries. Use canonical helpers when you want the type of value being inspected to be obvious in mixed examples or generated query text.

Type conversion and checking

FunctionBehaviour
toString(x)any → String; nullnull
toInteger(x) / toIntegerOrNull(x)Int/Float/String/BoolInt; OrNull form suppresses conversion errors
toFloat(x) / toFloatOrNull(x)Int/Float/StringFloat; OrNull form suppresses conversion errors
toBoolean(x) / toBooleanOrNull(x)Bool/String/IntBool; OrNull form suppresses conversion errors
x::TYPEpreferred explicit cast syntax
CAST(x AS TYPE)strict cast syntax
TRY_CAST(x AS TYPE)nullable cast; returns null on invalid input
type.of(x)name of the value's type, e.g. "INTEGER", "LIST<T>"
coalesce(a, b, )first non-null argument
temporal.timestamp() / timestamp()current Unix time in milliseconds
RETURN toInteger('42'),                         // 42
     toIntegerOrNull('abc'),                  // null
     '2024-01-15'::DATE,                      // DATE
     TRY_CAST('bad date' AS DATE),             // null
     toFloat(42),                             // 42.0
     coalesce(null, null, 'fallback'),        // 'fallback'
     type.of(1),                            // 'INTEGER'
     type.of([1, 2, 3]),                    // 'LIST<INTEGER>'
     type.of('2024-01-15'::DATE)            // 'DATE'

coalesce recipes

// Default a missing property
MATCH (p:Person) RETURN p.name, coalesce(p.nickname, p.name) AS display

;// Cascade through several optional fields
RETURN coalesce($phone, $email, 'unknown') AS contact

;// Replace null in ordering
MATCH (p:Person)
RETURN p.name, coalesce(p.rank, 999999) AS rank_for_sort
ORDER BY rank_for_sort

For multi-branch logic with arbitrary predicates per branch (not just "first non-null"), use CASE.

type.of recipes

// Filter a heterogeneous list to numbers only
MATCH (n)
WHERE all(x IN n.values WHERE type.of(x) = 'INTEGER')
RETURN n

;// Group by runtime type
UNWIND [1, 'two', 3.0, true, null] AS x
RETURN type.of(x) AS t, count(*) AS n
ORDER BY t

timestamp

Wall-clock milliseconds since the Unix epoch.

MERGE (c:Counter {name: 'events'})
ON CREATE SET c.first_seen = temporal.timestamp()
SET c.last_seen = temporal.timestamp()

See Data Types for every type.of return value and for how each type maps between LoraDB and host languages.

Conversion forms compared

All three forms below target the same type system, but they are meant for different reading and error-handling styles:

RETURN '2024-01-15'::DATE              AS preferred,
     CAST('2024-01-15' AS DATE)      AS casted,
     TRY_CAST('not a date' AS DATE)  AS nullable
  • value::TYPE is the preferred documentation style for handwritten query text.
  • CAST(value AS TYPE) is Cypher cast syntax and useful for query generators or users who prefer that form.
  • TRY_CAST(value AS TYPE) is the nullable form. It is the right tool for imports, user-provided filters, and optional parameters.

Legacy helpers such as toInteger remain intentionally documented for Cypher compatibility and simple scalar parsing. For typed construction of DATE, TIME, DATETIME, LOCAL_DATETIME, DURATION, POINT, and VECTOR, prefer casts.

Null propagation — the common thread

Most functions return null when any argument is null. A small handful don't, so they're worth memorising:

  • Aggregates (count, sum, …) skip null inputs (except count(*), which counts rows).
  • coalesce(a, b, …) — returns the first non-null argument.
  • temporal.timestamp() / timestamp(), math.pi(), math.e(), math.random() / random() — take no arguments.

Everywhere else, expect null in → null out. This is what makes IS NULL / IS NOT NULL essential over = null.

Quick lookup

Finding the right function for a task:

I want to…Reach for
Pick the first non-null valuecoalesce(a, b, )
Branch on arbitrary conditionsCASE WHEN THEN END
Count rows matching a conditioncount(CASE WHEN THEN 1 END)
Concatenate a list into a stringreduce over string.split / collect
Current time (ms)temporal.timestamp() / timestamp()
Current calendar daytemporal.today()
Name of a value's typetype.of(x)
Internal id of a node / relid(x)
Total order over temporal values<, <=, >, >= — see Ordering
Cartesian or geodesic distancegeo.distance(a, b)
Score a VECTOR against a query vectorvector.similarity(v, $q)
Signed distance under a metricvector.distance(a, b, EUCLIDEAN)
Magnitude of a VECTORvector.norm(v, EUCLIDEAN)
Dimension of a VECTORvector.dimension(v) or value.size(v)
Convert VECTOR coordinates back to a LISTvector.coordinates(v, FLOAT) / vector.coordinates(v, INTEGER)

Not supported

  • Compatibility utility namespaces — no external utility compatibility layer.
  • General-purpose procedures (CALL db.labels() etc.) — rejected at analysis time. The supported CALL surface today is limited to the vector and full-text index query procedures documented in Indexes.
  • User-defined functions — no registration surface.

Full list in Limitations.

See also