Query DSL
A query is a function of the class’s references, q, which mirror its fields. The compiler checks each one, so a
query over a field the class doesn’t have, or a comparison with a value of the wrong type, doesn’t compile:
const leads = await gom.loadAll(PersonCareer, (q) => ({ where: q.person.bio.contains('Lead'), orderBy: q.person.name.asc(), limit: 20}));A query may hold where, orderBy, limit, skip, seek and depth.
Conditions
Section titled “Conditions”// AND: a list of conditionswhere: [q.person.name.eq('Alice'), q.person.bio.isNotNull()]
// OR, AND and NOTwhere: anyOf(q.person.name.eq('Alice'), q.person.name.eq('Bob'))where: [q.issue.state.eq('open'), anyOf(q.issue.id.eq(123), q.assignedTo.name.eq('Alice'))]where: not(q.issue.locked.eq(true))
// a relationship's target, or a nested view's root and relationshipswhere: q.assignedTo.name.eq('Alice')where: q.raisedBy.worksFor.name.startsWith('Ac')A condition on a relationship holds when at least one related node meets it. Each condition is its own existence check,
so two conditions need not hold of the same related node. When they must, use any:
// propositions with at least one mention that resolves to this entitywhere: q.mentions.any((m) => m.resolvedId.eq(entityId))
// one mention that is both the subject and resolves to this entitywhere: q.mentions.any((m) => [m.role.eq('SUBJECT'), m.resolvedId.eq(entityId)])
// propositions with no such mention, including those with no mentions at allwhere: q.mentions.none((m) => m.resolvedId.eq(entityId))Operators
Section titled “Operators”| Kind | Operators |
|---|---|
| Comparison | eq, neq, gt, gte, lt, lte, in, notIn |
| Text | contains, startsWith, endsWith, matches (a regular expression; not on FalkorDB), containsIgnoreCase, eqIgnoreCase |
| Null | isNull(), isNotNull() |
| List property | hasItem(value): the value is in a list-valued property, the mirror of in |
| Logic | anyOf(...), allOf(...), not(...), a list for AND |
| Labels | instanceOf(Type): the node carries all the labels of a fragment class. hasAnyLabel('A', 'B'): it carries any of them |
| Order | asc(), desc(), then nullsFirst() or nullsLast() |
gt, gte, lt and lte are offered on numbers, texts and dates, and the text operators on texts alone.
Querying Fragments
Section titled “Querying Fragments”The DSL isn’t only for views. Every fragment has references to its own fields:
await gom.loadAll(ChunkNode, (q) => ({ where: q.sectionId.eq('sec-1') }));await gom.count(ChunkNode, (q) => ({ where: q.sectionId.eq('sec-1') }));await gom.loadAll(WebUser, (q) => ({ where: q.instanceOf(AnonymousWebUser) }));A reference uses the field’s name. Drivine maps it to the stored name, whether @GraphProperty renamed it or it is a
@NodeStamp field, and an entry of a @PropertyBag is reached through the bag: q.metadata.source.eq('wiki').
Runtime Keys
Section titled “Runtime Keys”When the property isn’t known until run time, such as an arbitrary bag key or a filter a caller names, give the stored name. Values are still bound as parameters:
where: q.property('metadata.source').eq('wiki')where: q.predicate('metadata.tags', 'HAS_ITEM', 'kotlin')A name that isn’t a plain identifier is quoted for you, and can’t break out of its quotes.
Ordering
Section titled “Ordering”orderBy: [q.person.name.asc(), q.person.uuid.asc()]orderBy: q.proposition.confidence.desc().nullsLast()Left to itself, the database decides where rows with no value go. Cypher ranks null above every value, so a descending
order starts with them. nullsFirst() and nullsLast() say otherwise. No index supplies that order, so leave it off a
property that is never null.
An order by an expression. A ranking computed from several properties is an expression over the root’s
properties, built from plus, minus, times, div, negate, exp, floor, coalesce, epochSeconds and
caseOf. Every value you give is bound. The ranking runs in the database with its limit, and later terms break its
ties:
orderBy: [ caseOf([above(q.validFrom, asOf).or(atMost(q.validTo, asOf)).then(0)], times(q.confidence, exp(negate(times(q.decay, floor(div(minus(epochSeconds(asOf), q.lastAccessedSeconds), 86400))))))) .desc(), q.id.asc()]An expression reads only the root’s properties, and is refused with seek.
Ordering a relationship’s list. An order on a relationship’s property sorts that list in each object, not the
roots: orderBy: [q.issue.id.desc(), q.assignedTo.name.asc()] orders the issues by id, and each issue’s assignees by
name. A nested view’s list sorts the same way: q.raisedBy.worksFor.name.desc(). This works the same on every engine
and needs no plugin. A recursive list can’t be ordered by the query: use @SortedBy, which orders every level.
@SortedBy sorts a list relationship after it is loaded, on every load of the view:
@GraphRelationship({ type: 'HAS_CONTRIBUTOR', target: () => Contributor })@SortedBy('name') // ascending; @SortedBy('name', false) for descendingcontributors: Contributor[];
@SortedBy('contributor.name') // a path into a related view or relationship fragmentNulls go last.
Limit and Pagination
Section titled “Limit and Pagination”limit and skip are applied in the database, after the order:
await gom.loadAll(PropositionView, (q) => ({ orderBy: q.proposition.created.desc(), limit: 20 }));await gom.loadAll(PropositionView, (q) => ({ orderBy: q.proposition.created.desc(), skip: 40, limit: 20 }));For a view, limit bounds the roots: each view still has all its relationships. Pair it with orderBy, or the subset
is arbitrary. count ignores limit and skip.
For large or changing results, page by keyset with seek. Its values follow the order’s terms, in the same order.
Drivine works out each comparison from the direction. End the order with a unique key, so that ties can’t be skipped or
repeated:
const page = await gom.loadAll(SessionView, (q) => ({ orderBy: [q.session.lastActivityAt.desc(), q.session.sessionId.desc()], seek: cursor ? [q.session.lastActivityAt.after(cursor.lastActivityAt), q.session.sessionId.after(cursor.sessionId)] : undefined, limit: pageSize + 1 // one more than a page tells you whether there is a next one}));- The ordered properties must not be null. A row whose key is null meets no comparison, so it drops out of every page after the first.
seekis refused without a matching order, with a null cursor value, withskip, with an order that places its nulls, and with an expression.- Index the cursor. A keyset is cheap only where the engine can seek into an index and stop. The index mirrors the
cursor: a range index over exactly the ordered properties, in order, such as
@RangeIndex({ properties: ['lastActivityAt', 'sessionId'] })on the class. Profiled on Neo4j with 200,000 nodes, a page from the middle took 27 database accesses with that index, 200,020 with an index on the first key alone, and 500,010 with none. - This is about Neo4j. Memgraph and FalkorDB sort between the scan and the limit whatever is indexed, so a keyset costs
what is left rather than a page.
seekis still worth having there, for pages that don’t shift under concurrent writes.
Drivine tells you when the index is missing. An ordered query is checked against the database’s indexes, and one that nothing mirrors is reported, once for each label and set of properties. Ordering without an index is correct, only unindexed, so the default is to warn. Turn it up in development or CI:
DrivineModule.withOptions({ connectionProviders: [DatabaseRegistry.buildOrResolveFromEnv()], query: { indexAdvice: 'FAIL' } // 'WARN' (the default) or 'OFF'});That applies to every manager the module makes. To change one manager: gom.indexAdvice = 'FAIL'. A query pinned to
one root, by an equality on the id or on a uniquely constrained property, is never advised on. The index list is read
once and kept; after making indexes as the application runs, call gom.indexAdvisor.refresh().

