Procedures and functions¶
Stored procedures and PSQL functions are called at /rpc/<name>.
| Routine | Called as | Response |
|---|---|---|
Selectable procedure (with SUSPEND) |
SELECT * FROM proc(...) |
array of rows |
| Executable procedure | EXECUTE PROCEDURE proc(...) |
object of output parameters, or 204 if it has none |
| PSQL function (Firebird 3+) | SELECT fn(...) FROM RDB$DATABASE |
a single JSON value |
Arguments¶
With GET, arguments come from the query string. With POST, they come from a JSON object:
curl 'localhost:3000/rpc/books_by_country?country=UK'
curl -X POST localhost:3000/rpc/place_order -H 'Content-Type: application/json' \
-d '{"customer_id":1,"book_id":2}'
curl 'localhost:3000/rpc/add_vat?amount=100' # 119.00
curl 'localhost:3000/rpc/add_vat?amount=100&rate=10' # 110.00
Arguments are matched by name. A missing argument is NULL, unless it and every argument after
it have defaults, in which case the defaults apply (place_order's qty = 1). Unknown argument
names in a POST body are rejected.
GET runs in a read-only transaction. Use POST for routines that change data.
Calling a routine many times¶
With Prefer: params=multiple-objects, a POST body that is an array of argument objects calls
the routine once per object, in one transaction:
curl -X POST 'localhost:3000/rpc/add_vat' -H 'Prefer: params=multiple-objects' \
-H 'Content-Type: application/json' -d '[{"amount":100},{"amount":100,"rate":10}]'
# [119.00,110.00]
| Routine | Response |
|---|---|
| Selectable procedure | the rows of every call together, as one result |
| Executable procedure | an array with the output parameters of each call, or 204 if it has none |
| PSQL function | an array with the value of each call |
- For a selectable procedure the calls are combined into one query, so
select, filters,order,limitandPrefer: countapply to the combined rows:POST /rpc/books_by_country?order=price&limit=5with[{"country":"UK"},{"country":"USA"}]returns the 5 cheapest books of both countries. The query calls the procedure once per row of a counting CTE, with each call's arguments as parameters, so it takes up to 1024 objects, as long as their arguments fit in 48 KB in all (FBRST128); split larger arrays. - The calls run in the request's transaction: if one fails, none of them is saved.
- A single object instead of an array is one call, and the response is still an array.
Passing the whole body¶
With Prefer: params=single-object, the raw JSON body is passed to a routine that has exactly one
text parameter (VARCHAR/CHAR or BLOB SUB_TYPE TEXT). Firebird has no JSON type, so the
routine receives the JSON document as text, for example to store it:
CREATE PROCEDURE log_request (payload BLOB SUB_TYPE TEXT) RETURNS (id INTEGER) AS
BEGIN
INSERT INTO api_log (payload) VALUES (:payload) RETURNING id INTO :id;
END
curl -X POST 'localhost:3000/rpc/log_request' -H 'Prefer: params=single-object' \
-H 'Content-Type: application/json' -d '{"event":"signup","tags":["a","b"]}'
# {"id":1}
The body must be valid JSON, and fit the parameter when it is a VARCHAR. A routine with another
signature is refused with FBRST129.
Raw bodies¶
A POST whose Content-Type is neither JSON nor a form puts the body, as it is, into the
routine's only parameter: text/* bodies as text, anything else as bytes. That's the way to
upload a file into a BLOB:
curl -X POST localhost:3000/rpc/blob_size -H 'Content-Type: image/png' --data-binary @cover.png \
-H 'Accept: text/plain'
# 173
curl -X POST localhost:3000/rpc/log_request -H 'Content-Type: text/plain' -d 'plain text, not JSON'
A routine with several parameters gets 400 (FBRST133): send a JSON object instead. The body
is limited to 64 MB.
Shaping selectable procedures¶
The output of a selectable procedure can be selected, filtered, ordered, paged and counted like
a table. With GET, query parameters that aren't argument names are treated as filters:
With POST, the arguments are in the body and the whole query string shapes the result.
Packages¶
Routines declared in a package header are available as /rpc/<package>.<routine>:
CREATE PACKAGE catalog AS
BEGIN
PROCEDURE search (term VARCHAR(100)) RETURNS (id INTEGER, title VARCHAR(200));
FUNCTION book_count (author_id INTEGER) RETURNS INTEGER;
END
curl 'localhost:3000/rpc/catalog.search?term=god'
curl 'localhost:3000/rpc/catalog.book_count?author_id=1'
Routines that exist only in the package body are private and not exposed.
Permissions¶
GRANT EXECUTE ON PROCEDURE books_by_country TO ROLE web_anon;
GRANT EXECUTE ON FUNCTION add_vat TO ROLE web_anon;
GRANT EXECUTE ON PACKAGE catalog TO ROLE web_anon;
-- a routine needs its own rights on the tables it uses
GRANT INSERT ON orders TO PROCEDURE place_order;
Errors¶
A PSQL EXCEPTION raised by the routine (or by a trigger it fires) becomes a 400 response. The
exception's message leads, its name is in hint, and where it was raised is in details:
CREATE EXCEPTION out_of_stock 'This book is out of stock';
-- in the procedure
IF (NOT EXISTS (SELECT 1 FROM books b WHERE b.id = :book_id AND b.in_stock)) THEN
EXCEPTION out_of_stock;
-- Firebird 3+: a routine needs USAGE on the exceptions it raises
GRANT USAGE ON EXCEPTION out_of_stock TO PROCEDURE place_order;
{"code":"HY000","message":"This book is out of stock","details":"At procedure 'PLACE_ORDER' line: 6, col: 5","hint":"exception OUT_OF_STOCK"}
The same applies to exceptions raised by triggers on tables you write to.