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:

ts
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 for

It 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 like stops at.
  • A term with no context. A body reading Expr.viewer or Expr.now is 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:

ts
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.