AshNeo4j.Cypher (AshNeo4j v0.10.1)

Copy Markdown View Source

AshNeo4j Cypher Functions for converting Elixir data structures to Cypher query components and running Cypher queries against a Neo4j database. Ideally has no specific knowledge of Ash

Summary

Functions

A dynamic label/type fragment :$(expr) (Neo4j ≥ 5.26) — the runtime-resolved counterpart to a static :Label in a node pattern (n:$(expr)) or a relationship pattern -[r:$(expr)]-> (#339). expr is any Cypher expression yielding the label/type: a parameter token ("$label"), a property ("n.kind"), or — for multiple labels in CREATE — a list-valued parameter ("$labels").

Converts a node variable, label, predicates and operator to cypher expression

Converts a node variable and labels to basic cypher node expression.

Converts a node variable and optional property map to cypher WHERE conditions and variable prefixed parameters map.

Converts a node variable, labels and optional property map to cypher properties string and variable prefixed parameters map.

Converts a node variable and optional property map to cypher properties string and variable prefixed parameters map.

Backtick-quotes a property name if it contains a dot, so that Neo4j parses it as a single property reference rather than a nested-property access. e.g. "location.point""`location.point`".

Converts a relationship variable, label and optional direction to cypher relationship.

Converts a list of property names into a remove properties string. The list is converted to a string in the format n.key1, n.key2.

Renders a %Cypher.Query{} to a {cypher_string, params} tuple.

:ok when the connected server supports Cypher 25 (negotiated server version ≥ 2025.06), else {:error, %AshNeo4j.Error.RequiresCypher25{}}. Use at the top of any function that emits Cypher 25-only syntax and thread the error up — a data layer returns it, never raises.

:ok when the connected server supports dynamic labels/types in pattern position ((n:$(expr)), -[r:$(expr)]-> in MATCH/CREATE/MERGE; negotiated server version ≥ 5.26), else {:error, %AshNeo4j.Error.RequiresDynamicLabels{}}. This is the server-feature axis (plain CYPHER 5), distinct from require_cypher25/0. Use at the top of any function that emits dynamic_label/1 and thread the error up — a data layer returns it, never raises.

Runs some cypher

Rewrites dots in a name as underscores so it is safe to use as a Cypher parameter key. Neo4j parses $foo.bar as the parameter $foo followed by a .bar property access, so the dot has to go.

Bare scalar Cypher for a vector function, e.g. for use in ORDER BY.

Functions

dynamic_label(expr)

@spec dynamic_label(binary()) :: binary()

A dynamic label/type fragment :$(expr) (Neo4j ≥ 5.26) — the runtime-resolved counterpart to a static :Label in a node pattern (n:$(expr)) or a relationship pattern -[r:$(expr)]-> (#339). expr is any Cypher expression yielding the label/type: a parameter token ("$label"), a property ("n.kind"), or — for multiple labels in CREATE — a list-valued parameter ("$labels").

expr must be a server-side Cypher expression, never an interpolated literal — that is the injection-safe form (the value is bound, not string-built). Gate emission with require_dynamic_labels/0.

Examples

iex> AshNeo4j.Cypher.dynamic_label("$label")
":$($label)"
iex> AshNeo4j.Cypher.dynamic_label("n.kind")
":$(n.kind)"

expression(variable, left, operator, right, opts \\ [])

Converts a node variable, label, predicates and operator to cypher expression

Examples

iex> AshNeo4j.Cypher.expression(:s, "name", "IN", "[$s_name_0]")
"s.name IN [$s_name_0]"
iex> AshNeo4j.Cypher.expression(:s, "name", "is_nil", true)
"s.name IS NULL"
iex> AshNeo4j.Cypher.expression(:s, "name", "is_nil", false)
"s.name IS NOT NULL"
iex> AshNeo4j.Cypher.expression(:s, "name", "contains", "$s_name_0")
"s.name CONTAINS $s_name_0"
iex> AshNeo4j.Cypher.expression(:s, "name", "contains", "$s_name_0", case_insensitive?: true)
"toLower(s.name) CONTAINS toLower($s_name_0)"
iex> AshNeo4j.Cypher.expression(:s, "name", "=", "$s_name_0", case_insensitive?: true)
"toLower(s.name) = toLower($s_name_0)"
iex> AshNeo4j.Cypher.expression(:n, "bounds", "within_bbox", "$test_point")
"point.withinBBox(n.`bounds.bbSW`, point({longitude: -180, latitude: -90}), $test_point) AND point.withinBBox(n.`bounds.bbNE`, $test_point, point({longitude: 180, latitude: 90}))"
iex> AshNeo4j.Cypher.expression(:n, "bounds", "within_bbox_box", {"$inner_sw", "$inner_ne"})
"point.withinBBox(n.`bounds.bbSW`, point({longitude: -180, latitude: -90}), $inner_sw) AND point.withinBBox(n.`bounds.bbNE`, $inner_ne, point({longitude: 180, latitude: 90}))"
iex> AshNeo4j.Cypher.expression(:n, "location", "st_distance", {"<", "$test_point", "$threshold"})
"point.distance(n.location, $test_point) < $threshold"
iex> AshNeo4j.Cypher.expression(:n, "location", "dwithin", {"$test_point", "$threshold"})
"point.distance(n.location, $test_point) <= $threshold"
iex> AshNeo4j.Cypher.expression(:s, "embedding", "vector_similarity", {">", "$s_embedding_0_vec", "$s_embedding_0_t"})
"vector.similarity.cosine(s.embedding, $s_embedding_0_vec) > $s_embedding_0_t"
iex> AshNeo4j.Cypher.expression(:s, "embedding", "vector_cosine_distance", {"<", "$s_embedding_0_vec", "$s_embedding_0_t"})
"(2.0 * (1.0 - vector.similarity.cosine(s.embedding, $s_embedding_0_vec))) < $s_embedding_0_t"

node(variable, labels)

Converts a node variable and labels to basic cypher node expression.

Examples

iex> AshNeo4j.Cypher.node(:s, [:Actor])
"(s:Actor)"

parameterized_conditions(variable, properties \\ %{})

Converts a node variable and optional property map to cypher WHERE conditions and variable prefixed parameters map.

Examples

iex> AshNeo4j.Cypher.parameterized_conditions(:n, %{name: "Bill Nighy"})
{"n.name = $n_name", %{"n_name" => "Bill Nighy"}}
iex> AshNeo4j.Cypher.parameterized_conditions(:n, %{name: "Bill Nighy", age: 72})
{"n.name = $n_name AND n.age = $n_age", %{"n_name" => "Bill Nighy", "n_age" => 72}}

parameterized_node(variable, labels, properties \\ %{})

Converts a node variable, labels and optional property map to cypher properties string and variable prefixed parameters map.

Examples

iex> AshNeo4j.Cypher.parameterized_node(:s, [:Actor])
{"(s:Actor)", %{}}
iex> AshNeo4j.Cypher.parameterized_node(:s, [:Cinema, :Actor], %{name: "Bill Nighy"})
{"(s:Cinema:Actor {name: $s_name})", %{"s_name" =>"Bill Nighy"}}

Note: the properties map is converted to parameter names by prefixing the keys with $<variable>, and the original values are returned in a separate map for use as query parameters.

parameterized_properties(variable, properties \\ %{})

Converts a node variable and optional property map to cypher properties string and variable prefixed parameters map.

Examples

iex> AshNeo4j.Cypher.parameterized_properties(:s)
{"{}", %{}}
iex> AshNeo4j.Cypher.parameterized_properties(:s, %{name: "Bill Nighy"})
{"{name: $s_name}", %{"s_name" =>"Bill Nighy"}}

quote_if_dotted(name)

Backtick-quotes a property name if it contains a dot, so that Neo4j parses it as a single property reference rather than a nested-property access. e.g. "location.point""`location.point`".

relationship(atom)

relationship(variable, label, direction \\ nil)

Converts a relationship variable, label and optional direction to cypher relationship.

Examples

iex> AshNeo4j.Cypher.relationship(:r, :ACTED_IN, :outgoing)
"-[r:ACTED_IN]->"
iex> AshNeo4j.Cypher.relationship(:r, :ACTED_IN, :incoming)
"<-[r:ACTED_IN]-"
iex> AshNeo4j.Cypher.relationship(:r, :KNOWS)
"-[r:KNOWS]-"

remove_properties(label, names)

@spec remove_properties(atom(), maybe_improper_list()) :: binary()

Converts a list of property names into a remove properties string. The list is converted to a string in the format n.key1, n.key2.

Examples

iex> AshNeo4j.Cypher.remove_properties(:n, [:born, :bafta_winner])
"n.born, n.bafta_winner"

render(query, opts \\ [])

Renders a %Cypher.Query{} to a {cypher_string, params} tuple.

Examples

iex> query = %AshNeo4j.Cypher.Query{
...>   clauses: [
...>     %AshNeo4j.Cypher.Match{pattern: "(s:Actor)"},
...>     %AshNeo4j.Cypher.Return{items: ["s"]},
...>     %AshNeo4j.Cypher.Limit{value: 5}
...>   ],
...>   params: %{}
...> }
iex> AshNeo4j.Cypher.render(query)
{"MATCH (s:Actor) RETURN s LIMIT 5", %{}}

iex> query = %AshNeo4j.Cypher.Query{
...>   clauses: [
...>     %AshNeo4j.Cypher.Call{
...>       branches: [
...>         "MATCH (s:Place) WHERE s.uuid = $b0_s_uuid_0 RETURN s",
...>         "MATCH (s:Place) WHERE s.uuid = $b1_s_uuid_0 RETURN s"
...>       ],
...>       union_type: :union_all
...>     },
...>     %AshNeo4j.Cypher.OptionalMatch{pattern: "(s)-[r]-(d)"},
...>     %AshNeo4j.Cypher.Return{items: ["s", "r", "d"]}
...>   ],
...>   params: %{"b0_s_uuid_0" => "x", "b1_s_uuid_0" => "y"}
...> }
iex> {cypher, _params} = AshNeo4j.Cypher.render(query)
iex> cypher
"CALL { MATCH (s:Place) WHERE s.uuid = $b0_s_uuid_0 RETURN s UNION ALL MATCH (s:Place) WHERE s.uuid = $b1_s_uuid_0 RETURN s } OPTIONAL MATCH (s)-[r]-(d) RETURN s, r, d"

require_cypher25()

@spec require_cypher25() :: :ok | {:error, struct()}

:ok when the connected server supports Cypher 25 (negotiated server version ≥ 2025.06), else {:error, %AshNeo4j.Error.RequiresCypher25{}}. Use at the top of any function that emits Cypher 25-only syntax and thread the error up — a data layer returns it, never raises.

require_dynamic_labels()

@spec require_dynamic_labels() :: :ok | {:error, struct()}

:ok when the connected server supports dynamic labels/types in pattern position ((n:$(expr)), -[r:$(expr)]-> in MATCH/CREATE/MERGE; negotiated server version ≥ 5.26), else {:error, %AshNeo4j.Error.RequiresDynamicLabels{}}. This is the server-feature axis (plain CYPHER 5), distinct from require_cypher25/0. Use at the top of any function that emits dynamic_label/1 and thread the error up — a data layer returns it, never raises.

WHERE-predicate label filtering is a finer gate

The WHERE n:$(expr) predicate form is not covered by this check: it fails on 5.26 (dynamic_labels: true) and only works on later servers (verified ≥ 2026.05). dynamic_labels?/0 / policy().dynamic_labels guarantee the pattern form only — matching bolty's flag semantics. A label-filter consumer must establish its own server gate (#339; capability-granularity question raised as diffo-dev/bolty#53).

run(query)

Runs some cypher

Examples

iex> cypher = "CREATE (n:Actor {name: 'Bill Nighy', born: 1949, bafta_winner: true}) RETURN n"
iex> {result, _} = AshNeo4j.Cypher.run(cypher)
iex> result
:ok
iex> cypher = "MATCH (n:Actor {name: $name}) RETURN n"
iex> params = %{name: "Bill Nighy"}
iex> {result, _} = AshNeo4j.Cypher.run(cypher, params)
iex> result
:ok

run(cypher, params \\ %{})

run_expecting_deletions(query)

run_expecting_deletions(cypher, params \\ %{})

sanitize_param(name)

Rewrites dots in a name as underscores so it is safe to use as a Cypher parameter key. Neo4j parses $foo.bar as the parameter $foo followed by a .bar property access, so the dot has to go.

vector_scalar(atom, variable, prop, vec_ref)

Bare scalar Cypher for a vector function, e.g. for use in ORDER BY.

vec_ref is the parameter reference holding the query embedding ("$q"). vector_similarity is Neo4j's normalised cosine similarity in [0, 1] (higher = closer); vector_cosine_distance rescales it to pgvector-style distance in [0, 2] (lower = closer) via 2 * (1 - similarity).

Examples

iex> AshNeo4j.Cypher.vector_scalar(:vector_similarity, :s, "embedding", "$q")
"vector.similarity.cosine(s.embedding, $q)"
iex> AshNeo4j.Cypher.vector_scalar(:vector_cosine_distance, :s, "embedding", "$q")
"(2.0 * (1.0 - vector.similarity.cosine(s.embedding, $q)))"