PL/Ruby lets you write PostgreSQL functions and triggers in Ruby. This document
describes the programming interface. For build and install instructions see
INSTALL; for a feature summary see README.
Tested on PostgreSQL 11 through 18 with Ruby 3.2 (shared library, non-thread-safe embedding).
- Enabling the language
- Writing functions
- Anonymous code blocks (DO)
- Data type mapping
- Composite types and records
- Arguments: IN, OUT, INOUT, TABLE, named
- Set-returning functions
- Trigger functions
- Event trigger functions
- Database access (SPI)
- Prepared statements
- Transaction control
- Subtransactions
- Quoting helpers
- Messaging: elog and pg_raise
- Shared data:
$_SHARED - Per-function data:
$_SD - Session initialization: on_init, modules and start_proc
- Errors and exceptions
- Security
CREATE EXTENSION plruby;PL/Ruby is an untrusted language, so the extension is superuser-only to install and only superusers can create PL/Ruby functions. See Security.
The body of a LANGUAGE plruby function runs as a top-level block. Inside it
you have:
args: a 0-indexed Array of the call arguments.argc: the number of declared arguments.- The function returns the value of its last expression (or an explicit
return). class,module, anddefstatements are allowed and define globally, so a class defined in one function is usable from another for the rest of the session (exactly like code loaded fromplruby_modules).
CREATE FUNCTION r_max(integer, integer) RETURNS integer
STRICT LANGUAGE plruby AS $$
args[0] > args[1] ? args[0] : args[1]
$$;
SELECT r_max(3, 7); -- 7STRICT (a.k.a. RETURNS NULL ON NULL INPUT) makes the function return NULL
automatically when any argument is NULL. Without it, a NULL argument arrives as
nil, so test with args[n].nil?.
You can run a one-off block of Ruby without defining a function, using DO:
DO $$
r = spi_exec('select count(*) as n from pg_class')
elog('NOTICE', "pg_class has #{spi_fetch_row(r)['n']} rows")
$$ LANGUAGE plruby;A DO block takes no arguments and returns nothing; use it for procedural
one-offs, migrations, or ad-hoc maintenance. Because PL/Ruby is untrusted, only
superusers can run DO ... LANGUAGE plruby.
Arguments are converted from their PostgreSQL representation to native Ruby values; return values are converted back using the declared return type's input function.
| PostgreSQL | Ruby value in args |
Ruby return value |
|---|---|---|
| smallint / integer / bigint / oid | Integer |
Integer (or any to_s-able) |
| real / double precision | Float |
Float |
| numeric | String (lossless) |
Numeric or String |
| boolean | true / false |
true / false |
| text / varchar / etc. | String |
String |
| bytea | binary String (ASCII-8BIT) |
String (raw bytes) |
arrays (e.g. int[]) |
nested Array |
nested Array |
| composite / row / record | Hash (string keys) |
Hash (or positional Array) |
| NULL | nil |
nil |
Arrays map naturally, including multidimensional arrays:
CREATE FUNCTION an_array() RETURNS int[] LANGUAGE plruby AS $$
[[1, 3, 5], [2, 4, 6]]
$$;
SELECT an_array(); -- {{1,3,5},{2,4,6}}Strings arrive tagged with the Ruby encoding that matches the database encoding
(UTF8 to UTF-8, LATIN1 to ISO-8859-1, EUC_JP to EUC-JP, WIN1251 to
Windows-1251, and so on), so length, reverse, and regexps operate on
characters, not bytes. Encodings Ruby does not recognize fall back to
ASCII-8BIT (binary), which is byte-preserving.
bytea is the exception: it is a raw byte string, so it arrives as a binary
(ASCII-8BIT) String holding its exact bytes, including any NULs, rather than
as hex text. Returning a String into a bytea takes the string's raw bytes
verbatim (whatever its encoding), with no hex or escape parsing. This holds
wherever a bytea crosses between SQL and Ruby: plain arguments and returns,
bytea[] elements, fields of composite arguments and results, and SPI result
rows. To emit specific bytes, build the String directly, e.g.
[0, 255, 16].pack('C*').
A composite argument arrives as a Hash keyed by column name (string keys); a
composite/record return value is built the same way.
CREATE TYPE tuple AS (name text, value integer);
CREATE FUNCTION make_tuple(text, integer) RETURNS tuple LANGUAGE plruby AS $$
{'name' => args[0], 'value' => args[1]}
$$;
SELECT * FROM make_tuple('answer', 42); -- answer | 42Functions declared RETURNS record must be called with a column definition
list, e.g. SELECT * FROM f() AS (a int, b text).
A composite may contain array fields and other composites: a composite field
whose value is a Hash is built recursively as a sub-record (to any depth), and
a function may return an array of composite (a Ruby Array of Hashes). In the
read direction a nested composite argument arrives as a nested Hash. A missing
Hash key and an explicit nil both become SQL NULL.
CREATE TYPE inner_t AS (a int, b text);
CREATE TYPE outer_t AS (id int, child inner_t);
CREATE FUNCTION nest() RETURNS outer_t LANGUAGE plruby AS $$
{'id' => 1, 'child' => {'a' => 2, 'b' => 'deep'}}
$$;
SELECT nest(); -- (1,"(2,deep)")By default json/jsonb travel as Strings of JSON text. The companion
extension jsonb_plruby provides a TRANSFORM FOR TYPE jsonb: a function
that opts in receives jsonb arguments as native Ruby data (Hash, Array,
String, Integer, Float, true/false, nil) and may return Ruby data
into a jsonb result, with no JSON.parse/JSON.generate round-trip, and
integers beyond Float precision survive exactly:
CREATE EXTENSION jsonb_plruby; -- requires plruby
CREATE FUNCTION redact(doc jsonb, key text) RETURNS jsonb
TRANSFORM FOR TYPE jsonb
LANGUAGE plruby AS $$
redact = lambda { |v|
case v
when Hash then v.to_h { |k, e| [k, k == key ? '[redacted]' : redact.call(e)] }
when Array then v.map { |e| redact.call(e) }
else v
end
}
redact.call(doc)
$$;Returned Hash keys and Symbols become JSON strings; any other Numeric
(e.g. BigDecimal) serializes losslessly through its text form. Unsupported
objects (and non-finite Floats) are rejected with a clear error. The
transform applies everywhere values cross between SQL and Ruby: plain
arguments and returns, fields of composite arguments and results, OUT
parameters, jsonb[] elements, return_next rows (including
RETURNS SETOF jsonb), and, when a trigger function declares the clause,
the $_TD['new']/$_TD['old'] rows and the 'MODIFY' result.
Rows read through spi_exec/spi_query are not affected; SPI results
keep their ordinary representation.
The hstore_plruby extension does the same for hstore
(TRANSFORM FOR TYPE hstore): arguments arrive as a Hash of String keys
to String-or-nil values, and a returned Hash becomes an hstore (keys
and values are stringified; nil becomes an hstore NULL). It requires
the hstore extension and is built from the hstore_plruby/ subdirectory.
The ltree_plruby extension does the same for ltree
(TRANSFORM FOR TYPE ltree): an ltree argument arrives as a Ruby Array of
its label Strings ('Top.Science' becomes ['Top', 'Science']), and a
returned Array becomes an ltree (its elements are stringified and joined
with ., so an invalid label is rejected by ltree's own parser). The empty
ltree maps to the empty Array. It mirrors the in-core ltree_plpython
transform, requires the ltree extension, and is built from the
ltree_plruby/ subdirectory.
CREATE EXTENSION ltree_plruby; -- requires plruby and ltree
CREATE FUNCTION ancestors(path ltree) RETURNS SETOF ltree
TRANSFORM FOR TYPE ltree
LANGUAGE plruby AS $$
(1..path.length).each { |n| return_next(path[0...n]) }
nil
$$;PL/Ruby supports the full range of argument modes.
- IN (the default) arguments appear in
args. - OUT / INOUT arguments: assign the result to a local variable named after the argument. A single OUT argument becomes the scalar result; multiple OUT arguments become the result row.
- TABLE(...) columns behave like OUT arguments for a set-returning function.
- VARIADIC tails arrive as a single
Arrayargument (the caller's extra arguments are folded into it, exactly as SQL sees them).VARIADIC "any"is not supported. - Named parameters are also available as local variables named after the
argument, in addition to positional
args.
CREATE FUNCTION add_sub(a integer, b integer, OUT sum integer, OUT diff integer)
LANGUAGE plruby AS $$
sum = a + b
diff = a - b
$$;
SELECT * FROM add_sub(10, 4); -- sum=14, diff=6Named arguments are aliased as locals only when the name is a valid Ruby
local-variable name (lowercase/underscore start). Others remain reachable
positionally through args.
Polymorphic types. anyelement and anyarray, and (on PostgreSQL 13 and
newer) anycompatible and anycompatiblearray, are accepted as argument and
return types. The concrete type is resolved at call time, so an argument
arrives as its native Ruby value (an Array for the array types) and the
return value converts back through the resolved type.
Declare the function RETURNS SETOF ... (or RETURNS TABLE(...)) and call
return_next once per output row.
CREATE FUNCTION squares(lim integer)
RETURNS TABLE(n integer, square integer) LANGUAGE plruby AS $$
(1..lim).each do |i|
n = i
square = i * i
return_next # emit current n, square
end
$$;
SELECT * FROM squares(3); -- (1,1), (2,4), (3,9)return_next(value) emits an explicit row (a scalar, an Array, or a Hash
matching the result columns). return_next with no argument emits a row built
from the current OUT/TABLE column variables.
A trigger function is declared RETURNS trigger. Trigger metadata and the rows
involved are available in the Hash $_TD:
| Key | Meaning |
|---|---|
$_TD['name'] |
trigger name |
$_TD['relid'] |
table OID |
$_TD['relname'] |
table name |
$_TD['schemaname'] |
schema name |
$_TD['when'] |
BEFORE, AFTER, or INSTEAD OF |
$_TD['level'] |
ROW or STATEMENT |
$_TD['event'] |
INSERT, UPDATE, DELETE, or TRUNCATE |
$_TD['new'] |
new row (INSERT/UPDATE), as a Hash |
$_TD['old'] |
old row (UPDATE/DELETE), as a Hash |
$_TD['argc'] |
number of trigger arguments |
$_TD['args'] |
trigger arguments, as an Array |
Return value of a BEFORE or INSTEAD OF ... FOR EACH ROW trigger:
nil: proceed with the operation using the unmodified row.'SKIP': silently skip the operation for this row.'MODIFY': proceed using the (modified)$_TD['new']row. Modify fields in place, e.g.$_TD['new']['col'] = 'value'. Only valid for INSERT/UPDATE; on a DELETE there is no new row, so returnnil(proceed) or'SKIP'instead.
On a DELETE trigger $_TD['old'] holds the row and $_TD['new'] is absent; on
an INSERT, $_TD['old'] is absent.
CREATE FUNCTION uppercase_name() RETURNS trigger LANGUAGE plruby AS $$
$_TD['new']['name'] = $_TD['new']['name'].upcase
'MODIFY'
$$;Statement-level TRUNCATE triggers are also supported: $_TD['event'] is
TRUNCATE, $_TD['level'] is STATEMENT, there is no row, and the return
value is ignored.
INSTEAD OF triggers on views work like BEFORE row triggers: the function
performs the real operation itself (typically with spi_exec against the base
tables) and returns nil to report the row as processed, or 'SKIP' to
suppress it. WHEN (...) conditions and deferrable constraint triggers behave
as they do for any trigger function.
An event trigger function is declared RETURNS event_trigger and fires on DDL
events rather than on table rows. Its $_TD carries:
| Key | Meaning |
|---|---|
$_TD['event'] |
the firing event, e.g. ddl_command_start |
$_TD['tag'] |
the command tag, e.g. CREATE TABLE |
CREATE FUNCTION no_drop() RETURNS event_trigger LANGUAGE plruby AS $$
pg_raise('error', 'dropping tables is not allowed') if $_TD['tag'] == 'DROP TABLE'
$$;
CREATE EVENT TRIGGER guard ON ddl_command_start EXECUTE FUNCTION no_drop();The return value of an event trigger function is ignored.
Run queries against the current database from within a function:
spi_exec(query [, limit]): executequery(optionally limiting rows) and return a result object. The call runs in a subtransaction that is rolled back automatically if the query raises an error.spi_fetch_row(result): return the next row as aHash, ornilwhen the rows are exhausted.spi_processed(result): number of rows the query produced.spi_status(result): the SPI status code as aString.spi_colnames(result): anArrayof the result's column names (Strings).spi_coltypes(result): anArrayof the columns' type OIDs (Integers); cast one to a name withoid::regtypein SQL.spi_coltypmods(result): anArrayof the columns' type modifiers (Integers), e.g.14forvarchar(10), or-1when none applies.spi_rewind(result): restart iteration from the first row.
The three spi_col* accessors are the counterparts of PL/Python's colnames,
coltypes, and coltypmods; each returns parallel Arrays over the result
columns. A result with no tuple set (a non-SELECT, such as a plain INSERT)
has no columns, so each returns an empty Array.
CREATE FUNCTION sum_series(n integer) RETURNS integer LANGUAGE plruby AS $$
r = spi_exec("select generate_series(1, #{args[0]}) as g")
total = 0
while (row = spi_fetch_row(r)); total += row['g']; end
total
$$;spi_exec materializes the whole result. To stream a large result without
holding it all in memory, use a cursor:
spi_query(query): open a cursor overquery. With a block it yields each rowHashand closes the cursor automatically; without a block it returns aPLRuby::Cursor.spi_fetchrow(cursor): the next row as aHash, ornilat end of result.spi_cursor_close(cursor): close a cursor early (otherwise it closes when exhausted or when the function returns). After closing,spi_fetchrowreturnsniland closing again is a no-op.PLRuby::CursorisEnumerable(via#each), somap,select, etc. work.
CREATE FUNCTION big_total() RETURNS bigint LANGUAGE plruby AS $$
total = 0
spi_query("select n from huge_table") { |row| total += row['n'] }
total
$$;Rows are read from the portal in batches, so memory stays bounded no matter how large the result. A query error during streaming is raised as a Ruby exception but is terminal for the surrounding statement (like a PL/pgSQL cursor loop).
For queries you run repeatedly, prepare a plan once and execute it with
parameters. spi_prepare takes the query text followed by the SQL type name of
each $1, $2, ... placeholder and returns a plan object:
spi_prepare(query, type1, type2, ...): returns a plan.spi_exec_prepared(plan, arg1, arg2, ...): execute the plan; returns a result object just likespi_exec.spi_query_prepared(plan, arg1, ...): stream the plan's result through a cursor instead of materializing it: with a block it yields each row and closes the cursor; without one it returns aPLRuby::Cursor. Closing the cursor leaves the plan reusable (spi_freeplanstays your job).spi_freeplan(plan): release the plan when you are done with it.
CREATE FUNCTION lookup(int) RETURNS text LANGUAGE plruby AS $$
plan = spi_prepare('select name from things where id = $1', 'int4')
row = spi_fetch_row(spi_exec_prepared(plan, args[0]))
spi_freeplan(plan)
row['name']
$$;A plan can be cached in $_SHARED and reused across calls within a session for
better performance; free it with spi_freeplan when no longer needed.
Inside a procedure invoked by CALL in a non-atomic context, you can commit
or roll back the current transaction:
spi_commit: commit the current transaction and begin a new one.spi_rollback: roll back the current transaction and begin a new one.
CREATE PROCEDURE import_batch() LANGUAGE plruby AS $$
(0...1000).each do |i|
spi_exec("insert into staging select * from source where batch = #{i}")
spi_commit # make each batch durable as it completes
end
$$;
CALL import_batch();These are only valid in a non-atomic call context. Calling them from an ordinary
function, or from a procedure invoked inside an explicit BEGIN/COMMIT block,
raises invalid transaction termination.
subtransaction { ... } runs a block inside an internal subtransaction and
returns the block's value. You may also pass a callable and arguments:
subtransaction(callable, arg, ...). If the block raises an exception, the
subtransaction's database changes are rolled back and the exception propagates
to the caller, where it can be rescued:
CREATE FUNCTION safe_insert(text) RETURNS text LANGUAGE plruby AS $$
begin
subtransaction do
spi_exec("insert into t(name) values (#{quote_literal(args[0])})")
raise 'empty name' if args[0] == ''
end
'inserted'
rescue => e
"skipped: #{e.message}" # the insert was rolled back
end
$$;Under PL/Ruby's error model, a database error inside the block (for example a
constraint violation surfaced by spi_exec) is also converted to a rescuable
Ruby exception after the subtransaction is rolled back, so rescue can recover
from it.
When building SQL dynamically, quote values and identifiers so the result is safe and syntactically correct:
quote_literal(string): quote a value as an SQL string literal.quote_nullable(value): likequote_literal, but a Rubynilbecomes the SQL keywordNULL.quote_ident(name): quote a string for use as an SQL identifier (only when needed).
sql = "select * from #{quote_ident(table)} where name = #{quote_literal(name)}"elog(level, message) emits a message at any PostgreSQL log level:
elog('DEBUG', 'detailed diagnostics')
elog('LOG', 'goes to the server log')
elog('INFO', 'informational')
elog('NOTICE', 'shown to the client')
elog('WARNING', 'something looks off')
elog('ERROR', 'stop right here') # aborts, like a PostgreSQL ERRORThe level is one of DEBUG, LOG, INFO, NOTICE, WARNING, or ERROR
(case-insensitive). Anything a function writes to standard output (puts,
print, $stdout) is also forwarded to the PostgreSQL log.
pg_raise(level, message, ...) accepts notice, warning, or error and
optionally attaches the structured error fields, like PL/pgSQL's
RAISE ... USING:
pg_raise('ERROR', 'order rejected',
detail: 'order 42 exceeds the credit limit',
hint: 'raise the limit or split the order',
sqlstate: 'P0001')detail: and hint: become the error's DETAIL and HINT lines; sqlstate:
(five characters, digits or A-Z) sets its SQLSTATE. A PL/Ruby caller
that rescues such an error gets all three back via PLRuby::Error#detail,
#hint, and #sqlstate (see Errors and exceptions).
$_SHARED is a Hash that persists across function calls within the same
database session. Use it to cache data or share state between PL/Ruby functions:
CREATE FUNCTION set_shared(key text, val text) RETURNS void LANGUAGE plruby AS $$
$_SHARED[args[0]] = args[1]
nil
$$;
CREATE FUNCTION get_shared(key text) RETURNS text LANGUAGE plruby AS $$
$_SHARED[args[0]]
$$;$_SD is a Hash private to each function that persists across calls to that
function within the same session. Where $_SHARED is one Hash shared by every
function (like PL/Python's GD), $_SD gives each function its own storage
(like PL/Python's SD), which is the natural place to cache a prepared plan or
other per-function state without risking a name collision with another function:
CREATE FUNCTION counter() RETURNS int LANGUAGE plruby AS $$
$_SD['n'] = ($_SD['n'] || 0) + 1
$$;Each function's $_SD is independent, and it is reset when the function is
recompiled (CREATE OR REPLACE). An anonymous DO block gets a fresh, empty
$_SD each time it runs; $_SHARED remains shared everywhere.
Three mechanisms let you run Ruby setup code the first time PL/Ruby is used in a
session. They run in this order: on_init, then modules, then start_proc.
on_init. The plruby.on_init configuration setting is a snippet of Ruby
source evaluated at the top level when the interpreter is first initialized,
before modules and start_proc. It is the counterpart of plperl.on_init, and
the natural place for requires and helper definitions that later setup relies
on:
SET plruby.on_init = 'require %q{json}; def tag(s); "[#{s}]"; end';Like start_proc, it is SUSET (only a superuser can set it). A constant or
top-level method it defines is visible to every function body in the session.
Modules. If a table named plruby_modules(modname text, modseq int, modsrc text) exists, its rows are evaluated at the top level (ordered by modname,
modseq) when the interpreter initializes. Any methods or classes the code
defines become available to every PL/Ruby function in the session, a
convenient place for a shared library of helpers:
CREATE TABLE plruby_modules (modname text, modseq int, modsrc text);
INSERT INTO plruby_modules VALUES
('util', 0, 'def slugify(s); s.strip.downcase.gsub(/\s+/, %q{-}); end');
-- slugify() is now callable from any PL/Ruby function in new sessions.start_proc. The plruby.start_proc configuration setting names a PL/Ruby
function to call once, when the interpreter is first initialized in a session:
-- e.g. in postgresql.conf, ALTER DATABASE ... SET, or a session that has
-- already loaded PL/Ruby:
SET plruby.start_proc = 'my_setup';Both run inside the first PL/Ruby call of the session, after the interpreter is ready.
Function bodies are syntax-checked at CREATE FUNCTION time by the validator;
an invalid body is rejected with the Ruby parse error.
At run time, an uncaught Ruby exception (for example, calling an undefined
method or a TypeError) is reported as a PostgreSQL ERROR, aborting the
statement. You can raise an error yourself with raise, elog('ERROR', ...),
or pg_raise('error', ...). Like every other procedural language, the error
carries a CONTEXT: PL/Ruby function "name" line (or PL/Ruby anonymous code block for a DO block) identifying the code that raised it.
A database error raised through the SPI functions (a failed spi_exec, a
constraint violation, and so on) arrives as a rescuable PLRuby::Error. The
exception's message is the PostgreSQL error message, and its sqlstate
method returns the original five-character SQLSTATE code:
CREATE FUNCTION divide(int, int) RETURNS text LANGUAGE plruby AS $$
begin
spi_exec("select #{args[0]} / #{args[1]}")
'ok'
rescue PLRuby::Error => e
e.sqlstate == '22012' ? 'division by zero' : "failed: #{e.sqlstate}"
end
$$;The exception also carries the error's DETAIL and HINT when present, via
PLRuby::Error#detail and #hint, including fields attached by a PL/Ruby
function further down the stack with pg_raise(..., detail:, hint:, sqlstate:).
sqlstate/detail/hint are nil on a PLRuby::Error that does not
originate from a database error (for example one raised by
elog('ERROR', ...)); ordinary Ruby exceptions do not have the methods at all.
PL/Ruby is an untrusted language. Historically a trusted variant could
restrict user code using Ruby's $SAFE levels and object tainting, but both
were removed in Ruby 3.0, so nothing sandboxes a PL/Ruby
function: it can do whatever the PostgreSQL server's operating-system user can
do: read and write files, open network connections, run shell commands, and so
on.
Accordingly, the language is created without the TRUSTED attribute: the
extension is superuser-only to install, and only superusers can create PL/Ruby
functions. Do not hand that ability to roles you would not trust with the
server's operating-system account.