Skip to main content

Query conditions

Which of MikroORM's filter operators this driver translates, and what they become in Cypher.

Applies to

mikro-orm-neo4j ≥ 0.3.0 · @mikro-orm/core 7.x · Neo4j ≥ 5

const books = await em.find(Book, {
price: { $gte: 10, $lt: 100 },
author: { name: 'Ann' }, // traverses the relation
search: { $fulltext: 'graph databases' }, // reads the index that field declares
});

Supported operators​

OperatorCypherNotes
$eqn.prop = $pAlso the plain form, { prop: value }. null becomes IS NULL.
$nen.prop <> $p$ne: null becomes IS NOT NULL.
$gt, $gte, $lt, $lten.prop > $p, …
$inn.prop IN $pAn array value is shorthand for it: { id: [1, 2] }.
$ninNOT (n.prop IN $p)Cypher has no negated membership operator.
$likeSTARTS WITH / ENDS WITH / CONTAINS, else =~See patterns.
$ilike=~ '(?is)…'Case-insensitive $like.
$ren.prop =~ $pSearches rather than full-matches — see patterns. A RegExp on the key itself carries its flags.
$existsIS NOT NULL / IS NULL
$and, $or, $notAND, OR, NOT (…)Nest freely; $not is allowed at the top level.
$overlapany(x IN n.prop WHERE x IN $p)Lists share at least one element.
$containsall(x IN $p WHERE x IN n.prop)On a string property it is Cypher's CONTAINS instead.
$containedall(x IN n.prop WHERE x IN $p)The stored list is a subset of the given one.
$sizesize(n.prop) or COUNT { … }A number or a comparison object ({ $gte: 3 }).
$some, $none, $everyEXISTS { … }, NOT EXISTS { … }On relations — see collections.
relation keyEXISTS { MATCH (n)-[:REL]->(t) WHERE … }{ tags: { name: 'Fiction' } }; nests to any depth.
$fulltextCALL db.index.fulltext.queryNodes(…)See full-text search.
raw()inlined fragment{ title: raw('toUpper(n.title)') }.

$hasKey, $hasKeys and $hasSomeKeys have no equivalent: Neo4j node properties hold primitives and lists, never maps, so there are no keys to test. Any operator the driver cannot translate throws by name — an unknown $operator used to fall through to an equality against the operator map itself, which quietly matched nothing.

Patterns​

$like maps onto a real Cypher string operator whenever the pattern allows, because those keep a TEXT/RANGE index usable and a regex never does:

PatternCypher
'Graph%'n.title STARTS WITH 'Graph'
'%graph'n.title ENDS WITH 'graph'
'%graph%'n.title CONTAINS 'graph'
'graph'n.title = 'graph'
'Gr_ph%s'n.title =~ '(?s)Gr.ph.*s'

\% and \_ match the character itself. $ilike always takes the regex form, with (?i).

$re searches inside the value, the way REGEXP does in SQL and $regex does in MongoDB. Cypher's =~ matches the whole string, so the pattern is wrapped ([\s\S]*(?:…)[\s\S]*) to keep the meaning the same across drivers — an anchored pattern still anchors:

await em.find(Book, { title: { $re: '^[Gg]raph' } });
await em.find(Book, { title: /^graph/i }); // flags travel inline as `(?i)`

Collections​

Relation filters take the collection operators, each becoming a subquery over the edge:

await em.find(Book, { tags: { $some: { name: 'Fiction' } } }); // EXISTS { … WHERE … }
await em.find(Book, { tags: { $none: {} } }); // NOT EXISTS { … }
await em.find(Book, { tags: { $every: { name: 'Reference' } } });
await em.find(Book, { tags: { $size: { $gte: 2 } } }); // COUNT { … } >= 2

$every is expressed as "none of them fails", which is also why a parent with no related records matches it — the same vacuous truth the SQL drivers produce. One collection operator per relation: two of them would be two subqueries over the same edge with no defined way to combine, so the driver throws and points at $and.

$size reads a list property as size(n.prop) and a relation as a COUNT subquery, so { keywords: { $size: 2 } } and { tags: { $size: 2 } } both mean what they look like.

Ordering​

orderBy reads a path the same way a filter does, and resolves it in the order that costs least:

await em.find(Book, {}, { orderBy: { author: { name: 'ASC' }, title: 'DESC' } });
What you order byWhat runs
A property of the entityORDER BY n.title
The related entity's primary keyORDER BY n.author — the key is denormalized onto the node, so nothing is traversed
A relation that is already matched (populated, or expanded by a stream)ORDER BY a.name on the variable the query already binds
Anything else through a to-one relationone OPTIONAL MATCH for the hop, then ORDER BY a.name

The hop is optional on purpose: an entity whose relation is missing still comes back, sorted among the nulls, the way a LEFT JOIN behaves. Cypher sorts nulls last ascending and first descending, and has no syntax to change that — so 'DESC NULLS LAST' and friends keep their direction and drop the qualifier.

Ordering by a to-many relation throws unless the query has already expanded it. A row per parent has no single child value to sort on, and picking one (the first? the smallest?) would sort by something you never asked for. em.stream() does expand its populated relations, so ordering by them there works and sorts the expanded rows — the same meaning a joined result set has in SQL. To order the children themselves, use populateOrderBy.

Relationship entities sort by their own edge properties, and may also reach through either endpoint (orderBy: { actor: { name: 'ASC' } }) — both nodes are already bound by the pattern.

Partial loading​

fields and exclude narrow what a node hands back, using a Cypher map projection — the properties left out never leave the server:

await em.find(Book, {}, { fields: ['title', 'author.name'], populate: ['author'] });
MATCH (this0:Book)
OPTIONAL MATCH (this1:Author)-[this2:WROTE]->(this0)
RETURN this0 { .id, .title, .author } AS node, this1 { .id, .name } AS rel_author
Written asProjected
fields: ['title']n { .id, .title } — the primary key is always included, since the entity is built around it
fields: ['address']every property the embeddable flattens to
fields: ['address.city']exactly that one
fields: ['author.name'].author on the root (so the relation resolves) and { .id, .name } on the author
fields: ['*']nothing — the node itself
exclude: ['note']everything the node stores except note
nothingnothing — RETURN n, exactly as before

That last row is deliberate: a read that narrows nothing emits no projection at all, so the common case keeps the cheapest possible plan. A single-table hierarchy also keeps its discriminator, which decides which entity each row becomes.

Projections are what make lazy mean something. A @Property({ lazy: true }) is left out of the projection until it is populated — before this, it travelled with every read and core simply discarded it.

$fulltext is a different shape from every operator above — it reads an index through a procedure rather than filtering a scan — and has a page of its own: full-text search.