Inspection

A small family of methods for asking about a relation rather than running it. They live on both Model (where they apply to Model.all) and on a Query relation so they compose with where, order, etc.

to-sql#

to-sql returns the SQL string that the relation would execute, with adapter-specific placeholders ($N for PostgreSQL, ? for SQLite and MySQL) left in. Bind values are not inlined.

say User.where({active => True}).annotate('debug').to-sql;

See Raw SQL and CTEs for the full set of escape hatches.

explain#

explain runs EXPLAIN against the underlying SELECT and returns the plan rows joined by newlines. SQLite uses EXPLAIN QUERY PLAN; PostgreSQL and MySQL use plain EXPLAIN.

say User.where({active => True}).order('lname').explain;
say User.explain;                  # whole-table plan

explain does not materialize the relation — it only EXPLAINs the SQL it would run, so it is cheap on large tables.

Relation predicates#

Method Returns
is-any True when the relation has ≥ 1 row
is-empty True when the relation has 0 rows
is-one True when the relation has exactly 1 row
is-many True when the relation has > 1 row
is-none True when the relation was explicitly scoped with .none
User.is-empty;                                 # nothing in the table?
User.where({active => True}).is-any;           # any active users?
User.where({email => $addr}).is-one;           # exactly one match?

is-none reports the scope, not the result: it is only true when the relation has been passed through .none, never just because the result set happens to be empty.

User.where({fname => 'Nobody'}).is-none;       # False — query just returns nothing
User.none.is-none;                             # True  — explicitly null-scoped

A .none-scoped relation short-circuits every predicate without touching the database. is-any, is-one, and is-many are always False; is-empty is always True.

cache-key, cache-version, cache-key-with-version#

These produce stable cache fingerprints for a relation, shaped like this:

cache-key                 →  "users/query-3f8a91c20b6e54de"
cache-version             →  "12-2026-05-12T14:23:11Z"
cache-key-with-version    →  "users/query-3f8a91c20b6e54de-12-2026-05-12T14:23:11Z"

cache-key is the table name plus an FNV-1a fingerprint of the SQL the relation would emit (template and bind values). Two relations producing the same SQL produce the same key; changing the WHERE, ORDER, or bind values changes it.

cache-version summarizes the result of the query as <count>-<max(updated_at)>. If the model has no updated_at column, cache-version is undefined (Str) and cache-key-with-version falls back to the bare cache-key. On a .none-scoped relation, the version short-circuits to "0" without touching the database.

my $key = User.where({active => True}).cache-key-with-version;

# Use it as a key in your own cache store:
my $payload = $cache.fetch-or-compute($key, {
  User.where({active => True}).all.map(*.serialize);
});

Because cache-key-with-version rolls forward whenever any row in the relation is updated, a cache entry written under this key invalidates on the next read.