Postgres
Every type, null, row count and parameter rule on Postgres
This page is the reference behind the main docs for Postgres: the place to look when a type, a | null, a row count or a parameter surprises you. MySQL and SQLite have the same page, with every answer in the same place.
| Versions | Postgres 18, 17 and 14 |
| URL | A postgres:// or postgresql:// URL, in DATABASE_URL or direct_url, a direct connection past a pooler such as PgBouncer |
| Type check | Reads the schema through postgres.js or pg, whichever your project has, or loads your schema files into PGlite |
| Drivers | postgres.js, pg, pg-promise, Bun.sql, PGlite, Neon and @vercel/postgres (Drivers) |
View the schema behind the examples
create extension if not exists pg_jsonschema;
create type user_role as enum ('admin', 'member');
create domain amount as numeric(12, 2) check (value >= 0);
create table users (
id serial primary key,
name text not null,
email text unique,
role user_role not null default 'member',
tags text[] not null default '{}',
created_at timestamptz not null default now()
);
create unique index users_email_lower on users (lower(email));create table orders (
id bigserial primary key,
user_id integer not null references users (id),
total amount not null,
paid boolean not null default false,
placed_at timestamptz not null default now(),
ships_on date,
receipt bytea
);create table events (
id bigserial primary key,
kind text not null check (kind in ('click', 'key')),
payload jsonb not null check (jsonb_matches_schema('{
"type": "object",
"properties": {
"x": { "type": "integer" },
"y": { "type": "integer" },
"code": { "type": "string" }
}
}', payload)),
at timestamp not null default localtimestamp
);Data types
Reylo decodes every value of its own queries itself, on every driver, so each SQL type has one TypeScript type and one form at run time. In each pair below the table, the SQL is a value and the JavaScript is what Reylo returns: select <SQL> as v gives the row { v: <JavaScript> }.
| SQL type | TypeScript | The value .run() returns |
|---|---|---|
smallint, integer, oid | number | 42 |
bigint, bigserial, count(*) | bigint | 9007199254740993n |
real, double precision | number | 1.1 |
numeric, money | string | "12.50", "$1.50" |
text, varchar, char, uuid | string | "Ada" |
boolean | boolean | true |
date | string | "2026-10-20" |
timestamp | string | "2026-10-20T10:00:00.123456" |
timestamptz | string | "2026-10-20T08:00:00.123456Z" |
time, timetz, interval | string | "10:00:00", "1 day 02:03:04" |
json, jsonb | unknown | { a: 1 } |
json, jsonb a JSON Schema CHECK holds | what the schema admits | { x: 10, y: 20 } |
JSON the statement builds (jsonb_build_object, jsonb_agg) | the object or array it builds | { total: 12.5 } |
bytea | Uint8Array | Uint8Array.of(1, 2, 255) |
| An array | an array of its element type | ["math", "poetry"] |
| An enum | the union of its labels | "admin" |
| A domain | its base type | its base type's |
| A row value | string | "(1,Ada)" |
Every other type (inet, ranges, point, tsvector, xml, …) | string | "[1,5)" |
Integers
A bigint is a JavaScript bigint, and so is count(*), which Postgres types bigint. Cast in the statement where you want a number:
import from "./pg_count_orders.sql";
const counts = await .();select count(*) as total, count(*)::int as total_number
from orderssum()
The sum() of an integer column is a bigint, and of a bigint or a numeric a numeric, so a string.
sum(2::integer)2nsum(2::bigint)"2"sum(2.5::numeric)"2.5"A cast that overflows
A cast to int that overflows is Postgres's error, never a wrapped number.
3000000000::bigint::intthrows PostgresError: integer out of rangeWriting a bigint out
JavaScript's JSON.stringify refuses a bigint, so write one out of your API as its text, String(v), or as the integer itself, JSON.rawJSON(String(v)), which is what .outputSchema describes.
9007199254740993::bigint9007199254740993nInside JSON the statement builds
A bigint is a JSON number, which JSON.parse rounds past 2^53 (JSON).
json_build_object('n', 9007199254740993::bigint){ n: 9007199254740992 }Decimals
A numeric is its exact text, "12.50", with the column's scale, and so is an avg(). A money is its text in the server's currency format, "$1.50".
32-bit floats
A real is a 32-bit float, given as the shorter text Postgres writes, except on Bun.sql in a statement with a parameter. A double precision is the same on every driver.
1.1::real1.11.1::double precision1.1On Bun.sql
In a statement with a parameter, Bun.sql reads the binary protocol and gives a real's exact value; in one without, it reads the text, as the other drivers do. Here the statement has a parameter.
1.1::real1.100000023841858Inside JSON the statement builds
A numeric is a JSON number, which JSON.parse reads as a double.
json_build_object('n', 12345678901234.123456::numeric){ n: 12345678901234.123 }Decimal parameters
A numeric parameter takes the text, "12.50" (What a parameter takes).
Dates and times
No value is a Date. A timestamptz is the instant in UTC, in ISO 8601 with a Z, and every type without a zone is the text Postgres writes, with a T between the date and the time of a timestamp:
import from "./pg_order_dates.sql";
const order = await .(, { : 1 });select placed_at, ships_on from orders where id = :idWhere your runtime has Temporal, Temporal.Instant.from() reads the instant, and Temporal.PlainDate.from() the date. Each text goes back in as a parameter as it is (What a parameter takes).
Fractions of a second
The fraction is the server's, to the microsecond, with its trailing zeros dropped, and none for a whole second.
'2026-10-20 10:00:00.123456+00'::timestamptz"2026-10-20T10:00:00.123456Z"'2026-10-20 10:00:00.5+00'::timestamptz"2026-10-20T10:00:00.5Z"'2026-10-20 10:00:00+00'::timestamptz"2026-10-20T10:00:00Z"An offset with seconds
An offset with seconds, as before standard time, moves to UTC exactly.
'1883-11-18 12:00:00-07:52:58'::timestamptz"1883-11-18T19:52:58Z"Infinity
The values infinity and -infinity stay those words, in every date and time type.
'infinity'::timestamptz"infinity"'-infinity'::date"-infinity"Years past 9999 or before 1 AD
A timestamptz takes ISO 8601's extended year, as Temporal.Instant writes it. A date or a timestamp keeps Postgres's text.
'12344-12-31 18:30:00+00'::timestamptz"+012344-12-31T18:30:00Z"'0002-06-01 12:00:00+00 BC'::timestamptz"-000001-06-01T12:00:00Z"'12345-01-01'::date"12345-01-01"'0002-06-01 BC'::date"0002-06-01 BC"'0002-06-01 12:00:00 BC'::timestamp"0002-06-01T12:00:00 BC"Past Temporal's range
Postgres reaches 294276 AD, and Temporal.Instant stops at 275760 AD, where Temporal.Instant.from throws a RangeError.
'294276-12-31 23:59:59+00'::timestamptz"+294276-12-31T23:59:59Z"Times and intervals
A time, a timetz and an interval are the server's text. Reylo reads the text at Postgres's default DateStyle (ISO) and IntervalStyle, so a session that sets others gets the text they write.
'10:00:00'::time"10:00:00"'10:00:00+02'::timetz"10:00:00+02"'1 day 02:03:04'::interval"1 day 02:03:04"Inside JSON the statement builds
A timestamptz is Postgres's text in the session's time zone, not the UTC form above. Here the session's TimeZone is America/Los_Angeles.
json_build_object('at', '2026-10-20 08:00:00.123456+00'::timestamptz){ at: "2026-10-20T01:00:00.123456-07:00" }On Bun.sql
A timestamp or a timestamptz keeps its milliseconds, not its microseconds (Drivers).
'2026-10-20 10:00:00.123456+00'::timestamptz"2026-10-20T10:00:00.123Z"Truth values
A boolean is a JavaScript boolean, and so is every comparison, IS NULL, EXISTS, IN, LIKE, NOT, AND and OR the statement computes. Postgres stores a real boolean, so no truth value is refused here, as it can be on MySQL and SQLite.
JSON
A json or jsonb value is parsed, and typed unknown, unless a JSON Schema CHECK holds the column or the statement builds the JSON. With the schema's CHECK, events.payload is what the schema admits, and kind, which a CHECK keeps to two strings, is their union:
import from "./pg_events.sql";
const rows = await .();select kind, payload from eventsJSON the statement builds with jsonb_build_object, json_build_object, jsonb_agg, json_agg or to_jsonb of a whole row is typed from its arguments, each value as JSON writes it:
import from "./pg_order_summaries.sql";
const rows = await .();select u.name,
jsonb_build_object(
'total', o.total,
'placed_at', o.placed_at,
'paid', o.paid
) as summary
from orders o
join users u on u.id = o.user_idThrough an aggregate the schema's type holds, and a whole row as JSON is typed column by column, an enum's labels included:
import from "./pg_events_by_kind.sql";
const groups = await .();import from "./pg_user_row.sql";
const row = await .(, { : 1 });select kind, jsonb_agg(payload order by id) as payloads
from events
group by kindselect to_jsonb(u) as "user" from users u where u.id = :id::intThe validators Reylo reads
The CHECK may call pg_jsonschema's jsonb_matches_schema or json_matches_schema, or the validator of four other extensions (jsonschema, postgres-json-schema, is_jsonb_valid, json_schema_validate). Reylo knows each by the extension that defines it, never by its name, and reads only the keywords that validator enforces, under the draft it applies.
Where the schema's type reaches
The schema's type reaches a bare read of the column, through a view or a subquery too; the column inside JSON the statement builds; a whole stored row (jsonb_agg(e) over from events e); and a parameter written whole to the column.
Where it stops
An expression over the column (payload -> 'x') is unknown, and so is a column under a union, and one whose CHECK was added NOT VALID.
Built with to_jsonb
One value turned to JSON by to_jsonb is unknown, and a whole row is the row's object.
Values inside a document
Inside a document a value is what JSON writes: integers and numeric are numbers, which JSON.parse rounds past 2^53 on every driver; a date is its text, and a timestamptz its text in the session's time zone, here America/Los_Angeles; a bytea is its hex text; a boolean is true or false.
jsonb_build_object('n', 9007199254740993::bigint){ n: 9007199254740992 }jsonb_build_object('n', 12.50::numeric){ n: 12.5 }jsonb_build_object('d', '2026-10-20'::date){ d: "2026-10-20" }jsonb_build_object('at', '2026-10-20 08:00:00+00'::timestamptz){ at: "2026-10-20T01:00:00-07:00" }jsonb_build_object('b', '\x0102ff'::bytea){ b: "\\x0102ff" }jsonb_build_object('ok', true){ ok: true }Aggregates over no rows
Over no rows, jsonb_agg is null, and its type says so: coalesce(jsonb_agg(…), '[]') is the array alone.
jsonb_agg(1) filter (where false)nullcoalesce(jsonb_agg(1) filter (where false), '[]')[]JSON parameters
A JSON parameter takes any JSON value, and a string is stored as a JSON string (What a parameter takes).
Binary
A bytea is a Uint8Array. Most drivers give Reylo a Buffer, which is one, so .run() returns it as it is.
Both text forms
Reylo reads both of Postgres's text forms of a bytea, the hex form of the default bytea_output and the escape form. The second pair's session sets bytea_output to escape.
'\x0102ff'::bytea<Buffer 01 02 ff>'\x0102ff'::byteaUint8Array(3) [ 1, 2, 255 ]Binary parameters
A bytea parameter takes a Uint8Array, a Buffer or not, on every driver.
Inside JSON the statement builds
A bytea is its text in the session's bytea_output: the hex form by default, and the escape form in the second pair, whose session sets escape.
json_build_object('b', '\x0102ff'::bytea){ b: "\\x0102ff" }json_build_object('b', '\x0102ff'::bytea){ b: "\\001\\002\\377" }Arrays
An array is a JavaScript array of its element's type, on every driver, an enum's and a domain's included:
import from "./pg_order_ids.sql";
const rows = await .();select u.name, u.tags, array_agg(o.id) as order_ids
from users u
left join orders o on o.user_id = u.id
group by u.idNULL elements
Postgres lets any array hold NULL, whatever the column's NOT NULL says. A text[] column is typed string[], and a NULL element comes back as null. Where the statement builds the array from values that may be NULL, as array_agg over a left join does, the type says so: (bigint | null)[].
'{a,NULL}'::text[][ "a", null ]The string NULL
A quoted "NULL" element stays a string.
'{"NULL"}'::text[][ "NULL" ]Several dimensions
Postgres does not record how many dimensions an array has, so a two-dimensional int[] is typed number[] and comes back as nested arrays. On Bun.sql, one in a statement with a parameter fails with Bun's own error, "Failed to read data", before Reylo sees it.
'{{1,2},{3,4}}'::int[][ [ 1, 2 ], [ 3, 4 ] ]Explicit bounds
An array with explicit bounds comes back as a plain array, its bounds dropped. On Bun.sql, one in a statement without a parameter fails with Bun's own error, "Failed to read data", before Reylo sees it.
'[0:1]={5,6}'::int[][ 5, 6 ]Arrays of row values
An array of row values is its text, as a row value is.
array[row(1, 'Ada')]"{\"(1,Ada)\"}"Array parameters
An array parameter takes a JavaScript array on every driver, for a SQL array (any(:ids), :tags::text[]) and for JSON (:data::jsonb) alike: Reylo writes it in the form the driver binds right.
Enums and domains
An enum is the union of its labels, and so is a text or varchar column whose CHECK keeps it to a list of strings (kind in ('click', 'key')). A domain is its base type: orders.total, an amount, is a numeric, so a string.
Arrays of an enum
An array of an enum is an array of the union.
'{admin,member}'::user_role[][ "admin", "member" ]Enum parameters
A parameter compared with an enum takes the union, and a value that is no label is Postgres's error, "invalid input value for enum".
Domains over JSON
A domain over jsonb whose CHECK holds it to a JSON Schema is what the schema admits.
Checks added NOT VALID
A CHECK added NOT VALID narrows nothing, since rows written before it may not hold.
Other types
Every other type is the text Postgres writes for it, a string: uuid, inet, cidr, macaddr, ranges and multiranges, geometric types, tsvector, xml, money, and a type an extension creates.
Fixed-length text
A char(n) is padded with spaces to its length.
'c'::char(3)"c "Row values
A row value, or a column of a composite type, is its text. Inside JSON the statement builds, a composite is an object of its fields.
row(1, 'Ada')"(1,Ada)"json_build_object('u', row(1, 'Ada')){ u: { f1: 1, f2: "Ada" } }Function results
A function's result has the type the function declares.
Values Reylo refuses
Every Postgres driver but Bun.sql hands Reylo each value's text, which Reylo decodes itself, so one value is refused, with an error that names its column:
Dates before 1 AD on Bun.sql
Bun decodes a date before 1 AD as an Invalid Date before Reylo sees it, and so a timestamp or a timestamptz in a statement without a parameter. Reylo refuses each rather than return a wrong value.
'0002-06-01 BC'::datethrows DecodeError: Column "v": the driver returned an Invalid Date, which is how Bun.sql decodes a date or timestamp before 1 AD. Reylo refuses it rather than return a wrong value.Nullability
A column is | null unless Reylo infers it never is. On Postgres, Reylo reads the catalog and the statement:
- A table's column is non-null where its
NOT NULLis valid and holds in every table that inherits from it. ANOT NULLaddedNOT VALIDtells Reylo nothing, since rows written before it may holdNULL. - An outer join makes the joined table's columns nullable: the right side of a
left join, the left side of aright join, both sides of afull join. - A test in the
wherethat fails onNULLmakes the column non-null, and a later inner join'sonshows the joined row exists:where o.total > 0,where email is not null,where lower(email) = :email. - An expression is non-null when Reylo's rules infer it:
- a literal, a cast,
count,existsandis null; - concatenation with
||, a comparison and arithmetic over non-null operands; - a built-in function that returns
NULLonly for aNULLargument (length,lower,round, …); - a
coalescewith a non-null argument; - a
casewhose branches andelseare all non-null.
- a literal, a cast,
- An aggregate other than
countisNULLover no rows, so it is nullable without agroup by. With one, every group has a row:- the aggregates that write a
NULLinto what they build,array_agg,json_agg,jsonb_aggandjsonb_object_agg, are non-null; - those that skip a
NULL,sum,max,string_aggand the others, are non-null where their argument is; - an aggregate with a
filterstays nullable, since the filter can leave a group no rows.
- the aggregates that write a
- A table's whole row (
select e,jsonb_agg(e)) is neverNULLunless an outer join nulls it, and keeps its columns'NOT NULLand theirCHECKlists. - A scalar subquery is non-null only when it is an aggregate without a
group byover a non-null value, as(select count(*) …)is. - A column of a
unionis non-null when it is in every arm. - An operator, function or cast your database defines, or an extension does, tells Reylo nothing: the rules stand down wherever Postgres could pick one of those over the built-in.
import from "./pg_user_totals.sql";
const rows = await .();select u.name, o.total, coalesce(o.paid, false) as paid
from users u
left join orders o on o.user_id = u.idA test in the where shows the joined row exists, so its columns are non-null; a window function is typed like any other, lag nullable on the first row:
import from "./pg_paid_orders.sql";
const rows = await .();import from "./pg_order_gaps.sql";
const rows = await .();select u.name, o.total, o.placed_at
from users u
left join orders o on o.user_id = u.id
where o.paidselect id, placed_at,
placed_at - lag(placed_at) over (order by placed_at) as since_previous,
row_number() over (order by placed_at) as n
from ordersWhere you know more than Reylo can infer, say it in the statement: coalesce(x, …) gives a default, and a where x is not null test both filters and makes the column non-null.
Row counts
How many rows .run() resolves to comes from what Reylo infers the statement can return:
| Resolves to | cardinality | On Postgres, when the statement |
|---|---|---|
Row | one | is an aggregate without a group by, or inserts one row with values (…) and has returning, an on conflict do update included |
Row | null | maybeOne | matches every column of a primary key or unique index with =, through joins on the joined table's key; has limit 1 or fetch first 1 row only; is a distinct or group by over values the where pins; is an update or delete by key with returning; or inserts one row with on conflict do nothing and returning |
void | none | is an insert, update, delete or merge without returning |
Row[] | many | is anything else |
A unique index on an expression counts when the statement compares the same expression, and a partial unique index when the where holds its predicate:
import from "./pg_user_by_email.sql";
const user = await .(, { : "Ada@example.com" });select id, name from users where lower(email) = lower(:email)- The expression is matched as Postgres stores it in the index. An index over a domain column stores a cast,
lower((email)::text), and the statement must write it the same way to match. - A partial unique index without its predicate in the
wherecounts for nothing:where slug = :slugover an index onslugwhere not deletedisRow[]. - Part of a composite key counts for nothing, and neither does a
having, or aninsert … select … returning. - At run time a
Rowstatement throws unless it gets exactly one row, and aRow | nullstatement throws when it gets more than one.
Parameters
Placeholders
Write each parameter as :name. Reylo sends $1, $2, … and .run() takes an object keyed by name. A statement may use Postgres's own $1 instead, never beside :name, and the object's keys are then the numbers, { 1: 42 }.
- Each parameter appears once in a statement, so that one place types it. To use a value twice, select it once in a
withand read it from there. - A
$nameis no placeholder on Postgres: Reylo's error says to write:name.
What a parameter takes
Every value .run() returns goes back in as a parameter of its type, on every driver, so a row can be passed straight to the next statement; a null goes back in where the parameter takes one (Parameters that may be null):
| SQL type | A parameter takes |
|---|---|
smallint, integer | number |
bigint | number, bigint or a string of digits |
numeric | string |
boolean | boolean |
date, timestamp, time | string |
timestamptz | a string that names its zone |
json, jsonb | any JSON value, or what the column's JSON Schema admits |
bytea | Uint8Array |
| An array | an array of its element's parameter type |
| An enum | the union of its labels |
- The parameters of
limit :nandoffset :narebigint, so they take anumbertoo. - A
timestamptzparameter takes the text of an instant that names its zone, as.run()returns it:"2026-10-12T19:06:48Z","2026-10-12 19:06:48+09","2026-10-12 19:06:48 Asia/Tokyo", orinfinity,-infinity,epochandnow. Pass aDateasdate.toISOString(). - Text without a zone is refused by
.run()before the statement runs, since the server would read it in the session's time zone. - A
timestampor adateparameter refuses aDate: drivers write aDate's time in different zones, the app's, the server's or UTC. - Postgres refuses the extended years a
timestamptzcomes back with,+010000-…and0000, so Reylo writes them as10000-…and… BC, and the value goes back in. - A JSON parameter reaches the database as its JSON text on every driver, so a string is a JSON string:
'{"a":1}'is stored as the string"{\"a\":1}". To pass JSON text you already hold, cast it from text::data::text::jsonb. - A typed JSON parameter,
:events::jsonb('…'), reaches Postgres as$1::jsonb(or$1::json) holding its JSON text, after.run()has filled in its schema's defaults and checked the value against the schema. - An array's elements never take
null, and.run()refuses an array that holds one:id = any(:ids)matches nothing for aNULLelement.
Parameters without a type
Postgres types every parameter from where the statement uses it:
- the column it is compared with or written to;
- the function it is passed to;
- the cast around it.
A bare select :x is text, so a string. Where nothing types it, as in select :x is null or concat(:x, '!'), Postgres refuses the statement, "could not determine data type of parameter" (42P18), until a cast types it: :x::text.
Parameters that may be null
A parameter takes null in two places:
- as the first argument of
coalesce, with a cast or without; - written whole, with no cast, to a column that may hold
NULL.
A cast is the parameter's whole type, so :email::text never takes null, and a test for null (:role is null, is not distinct from) makes nothing nullable. A column holds NULL unless it is NOT NULL, its domain is declared not null, or a check (… is not null) holds it:
import from "./pg_users_by_role.sql";
const = await .(, { role: null });select id, name from users
where role = coalesce(:role::user_role, role)Casts
A :: cast is Postgres's own, and Reylo sends it as written: :n::int8, :tags::text[], :at::timestamptz. So is cast(:n as int8). A cast types a parameter wherever its context does not, and the parameter takes what its cast's type takes.
How the text is read
Reylo reads the statement as Postgres does: a :name is a placeholder only outside single-quoted strings (with ''), E'' strings (with backslashes), dollar-quoted strings ($$…$$, $tag$…$tag$), double-quoted names, and line and nested block comments, and a :: is a cast.
Drivers
Each driver reads Postgres's protocol as it always does, and Reylo makes only the choice per type that follows, for its own query, on the client you pass. A parser or option you set for your own queries does not reach Reylo's, and none of Reylo's reaches yours.
| Driver | How Reylo reads its values | Refused | Lost before Reylo sees it |
|---|---|---|---|
pg | A decoder table with each query that answers every type | binary: true | |
pg-promise | The same table, through .result() | A receive event that rewrites results | |
| Neon, Vercel Postgres | The same table, with each query | ||
| PGlite | A parser table with each query, naming the statement's types | ||
| postgres.js | Each value's text (.raw()), the row built with the query's own column names | A transform.row | |
Bun.sql | Bun's own values, converted | A timestamp's and a timestamptz's microseconds, and a value before 1 AD; it throws on a NULL inside an int4[] or float4[], and on two array shapes (below) |
- A postgres.js
transformthat renames your columns does not rename Reylo's. - On
Bun.sql, a statement with a parameter is read over the binary protocol and one without as text. So arealis its exact float,1.100000023841858, only with a parameter, and atimestampor atimestamptzbefore 1 AD is refused only without one; adatebefore 1 AD is refused either way. - Bun fails with its own error, "Failed to read data", before Reylo sees the value, on a two-dimensional
int[]in a statement with a parameter and on an array with explicit bounds in one without. - The
@vercel/postgrespackage is deprecated on npm: Reylo's types accept its client, and its tests do not run it. - Each driver takes its transactions and connections too:
pg's Pool, Client and PoolClient; postgres.js's andBun.sql's.begin(),.savepoint()and.reserve(); PGlite's.transaction(); Neon's Pool and Client; andpg-promise's.task()and.tx().
The type check reads the schema through postgres.js or pg, so an app on PGlite, Neon, Bun.sql, pg-promise or @vercel/postgres adds one of them as a dev dependency.
Limits
The edges of what Reylo types on Postgres, each with what to write instead:
- A function's body. A SQL-language function is typed from its declaration (
returns), never from its body, and a PL/pgSQL body is not read at all. Declare the result you return. - An array's dimensions and elements. An
int[]isnumber[]however many dimensions it holds, and its elements are typed withoutnullunless the statement builds it. Keep an array one-dimensional, or build it witharray_agg. - A row value outside JSON. It is its text. Select its fields as columns, or build an object with
to_jsonborjsonb_build_object. - A parameter in place of a name. A parameter is a value:
select * from :tableis Postgres's syntax error, andorder by :columnsorts by a constant. Choose a column withcase :sort when 'name' then name end. - A statement nested past 1,000 levels of the parse tree (about 496 terms of
a + b + …, or 164 nested subqueries), which Reylo types from the server's description alone: any number of rows, every column nullable. - One statement per file. A file holds one
select,insert,update,delete,merge,valuesortable. A file of several statements or of DDL, such as a migration, is an error in the type check.