Reading tables¶
Every table and view is exposed at /<name>. Global temporary tables are included; system tables
are not.
Names¶
Firebird stores unquoted identifiers in upper case. The API shows those in lower case, so table
BOOKS is /books and column AUTHOR_ID is author_id. Identifiers created with quotes keep
their exact spelling: a table "Events Log" is /Events%20Log, with a column "When" as When.
Lookups also accept the database spelling (/BOOKS).
Selecting columns¶
| Request | Meaning |
|---|---|
GET /books |
all columns (select=*) |
GET /books?select=id,title |
chosen columns, in this order |
GET /books?select=book:title |
rename: alias:column |
GET /books?select=price::text,published::int |
cast: column::type |
GET /books?select="Odd Name" |
quote names that contain special characters |
Casts: text, varchar, integer, smallint, bigint, int128, numeric, double,
decfloat, boolean, date, time, timestamp.
Filtering¶
A query parameter named after a column filters on it: column=operator.value. Several filters
are combined with AND, and one column may be filtered several times
(?price=gt.5&price=lt.10).
| Operator | SQL | Example |
|---|---|---|
eq, neq |
=, <> |
?status=eq.new |
gt, gte, lt, lte |
>, >=, <, <= |
?price=lt.10 |
like |
LIKE, with * as the wildcard |
?title=like.The* |
ilike |
case-insensitive LIKE |
?title=ilike.*god* |
similar |
SIMILAR TO |
?code=similar.[A-Z]{3} |
containing |
CONTAINING (case-insensitive substring) |
?summary=containing.dragon |
starting |
STARTING WITH |
?title=starting.The |
in |
IN (...) |
?id=in.(1,2,3), ?name=in.("a,b",c) |
is |
IS NULL, IS NOT NULL (not_null), IS TRUE/FALSE/UNKNOWN |
?born=is.null |
isdistinct |
IS DISTINCT FROM |
?a=isdistinct.5 |
not.<op> |
NOT (...) |
?id=not.in.(1,2) |
containing, starting and similar are Firebird operators that PostgREST does not have.
starting can use an index on the column; like/ilike patterns, containing and similar
scan the rows.
Values are converted to the column type: booleans accept true/false/t/f/1/0, dates and
timestamps accept ISO 8601 (2024-05-06, 2024-05-06T07:08:09Z). Wrap values that contain
commas or parentheses in double quotes inside in.(...).
Logic trees¶
or and and take a parenthesised list of conditions written as column.operator.value. They
nest, and not. negates a group:
?or=(price.lt.8,and(published.gte.1990,in_stock.is.true))
?not.and=(id.gt.2,id.lt.5)
?or=(title.ilike.*sea*,title.ilike.*star*)
?or=(born.is.null,country.not.eq.UK)
Inside a tree, negate a single condition with column.not.operator.value.
Ordering¶
Without order, rows come in whatever order Firebird returns them.
Ordering by an embedded resource¶
Order by a column of a to-one (many-to-one) embed with embed(column). The embed must be in
select; use its alias if it has one:
GET /books?select=title,authors(name)&order=authors(name).desc,title
GET /books?select=title,writer:authors(name,born)&order=writer(born)
GET /books?select=title,...authors(name)&order=authors(name)
It works for embedded rows too. This orders each tag's books by their author's name:
Filters on the embed apply: a row whose embed is filtered out, and so returned as null, sorts
as NULL. Place those rows with nullsfirst or nullslast.
To-many embeds (one-to-many, many-to-many) have no single value to order by and are refused with
FBRST126; order their rows with embed.order= instead (see
Resource embedding).
Paging¶
or the Range header (items, zero-based, inclusive):
Firebird runs these as OFFSET n ROWS FETCH NEXT m ROWS ONLY. Large offsets get slower, because
Firebird has to skip those rows. For deep paging, filter on an indexed key instead
(?id=gt.<last id>&order=id&limit=25).
db-max-rows caps every read, whatever the request asks for.
A large page is encoded as its rows are read, so a result costs about its JSON size in memory
on the server (a 100 MB response peaks near 200 MB of process memory); the response is sent
once complete, because Content-Range needs the number of rows. Reads with embedded resources
keep their rows until the children are attached and cost more. For exports, page with limit
and offset or a Range header.
Counting¶
Send Prefer: count=exact to get the total in Content-Range:
The status is 206 Partial Content when the page doesn't contain every matching row, and 200
otherwise. Counting runs an extra SELECT COUNT(*), which reads every matching row; only ask for
it when you need it.
Estimated counts¶
On large tables an exact count is slow, because Firebird has to visit every row. Two cheaper modes return an estimate instead:
Prefer |
Total in Content-Range |
|---|---|
count=exact |
the exact number of matching rows |
count=planned |
the table's row count estimated from its index statistics, without reading the table |
count=estimated |
exact up to db-max-rows (or 1000 when it is not set), the planned estimate above that |
The estimate comes from the statistics of the table's primary key or another unique index: their
selectivity is 1 / number of rows. Firebird computes index statistics when an index is created and
when you run SET STATISTICS INDEX, so the estimate is as old as that. Recompute them after large
loads or deletes, for example in a nightly job. This recomputes every index of the database (in
isql, wrap it in SET TERM):
EXECUTE BLOCK AS
DECLARE name VARCHAR(63);
BEGIN
FOR SELECT TRIM(RDB$INDEX_NAME) FROM RDB$INDICES WHERE COALESCE(RDB$SYSTEM_FLAG, 0) = 0 INTO :name DO
EXECUTE STATEMENT 'SET STATISTICS INDEX "' || name || '"';
END
An estimate only describes a whole table. When it doesn't apply, the total is exact and
Preference-Applied says count=exact:
- the request has filters (an estimate of a filtered count would need the optimizer's guesses, which Firebird does not expose);
- the resource is a view or a procedure;
- the table has no unique index, or its unique indexes have no statistics yet (an index created on
an empty table has none until the next
SET STATISTICS); - with
count=estimated, the estimate is below the rows already counted.
The total is never smaller than the rows in the response: when an old estimate is behind, the rows actually returned win.
HEAD¶
HEAD runs the same query and returns only the headers, so HEAD with Prefer: count=exact
gives the count without a body.
Transactions¶
Every request runs in its own transaction. GET and HEAD use a read-only transaction, so
a view or selectable procedure that tries to write fails instead of changing data.