Remote / foldkit-remote-server
Reference semantics: evaluate and the conformance suite
Running a query body over rows you already hold
A descriptor declared with
Query.define carries its meaning as a
value (body), and evaluate runs a body directly:
import { Entity, Expr, Order, Query, evaluate } from 'foldkit-entity'
const Post = Entity.define('Post', Schema.Struct({ id: Schema.String, slug: Schema.String }))
const intro = Query.from(Post).pipe(
Query.where(Expr.eq(Post.fields.slug, 'intro')),
Query.orderBy(Order.asc(Post.fields.id)),
)
evaluate(intro, {}, [{ id: 'p1', slug: 'intro' }, { id: 'p2', slug: 'outro' }])
// [{ id: 'p1', slug: 'intro' }]: the matching rows, in the order the body asks forIt lives in foldkit-entity, beside the IR it interprets, because
it is that IR's reference semantics rather than anything this package does —
and because a client judging rows it already holds needs it without depending on
a server package. This package re-exports it, so importing it from either place
means the same thing.
It is pure — it reads the rows it is given and nothing else — and it is the
reference the compiled interpreters are checked against. foldkit-remote-drizzle
turns the same body into SQL; the test suite runs both over the same rows in a
real SQLite and Postgres and requires the same ids in the same order. That is
what makes a body source-neutral in fact rather than in principle.
It follows SQL, not JavaScript. The case that matters is null: null = null
is unknown in SQL and matches no row, where JavaScript would call the two
equal. A comparison keeps a row only when it is true, so a null on either side
drops it. Where an ordering puts null is the term's to say
(Order.asc(field, { nulls: 'first' })); unsaid, it is Postgres's (last
ascending, first descending), which the SQL compiler writes out for SQLite too.
What it refuses rather than guesses, each where the query runs:
- Ordering by values it has no order for, such as a number in one row and text in another, or by a backend's locale collation, which only that backend has. Every term is checked over every matched row before anything is sorted, so the refusal is the same on every engine, whichever pair a sort would have compared first. One matched row, or none, has nothing to order.
- NaN, which SQLite stores as null and Postgres orders after every number.
- Text holding a NUL character in a comparison or a search, which Postgres
text cannot hold and SQLite's
likestops at. - A term with no context. A body reading
Expr.viewerorExpr.nowis refused when run without them (ContextMissingError), never read as null.
Checking an interpreter against the semantics
evaluate is the reference, and cases (in foldkit-entity/conformance) is
what it is a reference to: the conformance suite, exported so an interpreter
written elsewhere can be held to the same semantics. Here run is your
interpreter:
import { cases, rows } from 'foldkit-entity/conformance'
for (const { what, body, input, expected } of cases) {
expect(run(body, input, rows).map(row => row.id), what).toEqual(expected)
}Every case is chosen to make interpreters disagree, which is the only kind
that tests anything: mixed case, nulls on both sides of a comparison, % and
_ as literal text, the empty search, a predicate compared to a boolean either
way round. That is not caution for its own sake — contains reached a released
package meaning three different things because every value in the original
fixture happened to be lowercase.
Both interpreters here run it: this package over rows in memory, and
foldkit-remote-drizzle compiled to SQL against a real SQLite and Postgres.