Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to PL/Ruby are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and the project aims to follow [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added

- **`$_SD`: per-function static storage.** A `Hash` private to each function that
persists across calls to that function within a session, the counterpart of
PL/Python's `SD` (where `$_SHARED` is `GD`). Each function's `$_SD` is
independent and resets when the function is recompiled; an anonymous `DO` block
gets a fresh, empty `$_SD` each run.

## [2.4.0] - 2026-07-06

Feature parity with the sibling PL/php 2.4: an `on_init` hook, the
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ PG_CPPFLAGS = -I$(RUBY_ARCHHDRDIR) -I$(RUBY_HDRDIR)
SHLIB_LINK = -L$(RUBY_LIBDIR) -L$(RUBY_ARCHLIBDIR) $(RUBY_LIBARG) $(RUBY_LIBS)

# Regression tests. "init" installs the extension; keep it first.
REGRESS = init base types numspecial bytea composite encoding datetime jsonb misc variadic shared trigger trigger2 spi raise errors sqlstate errcontext cargs pseudo anycompat srf out varnames validator replace classes prepare cursor hostile compat txn evttrig subxact modules quote require cookbook stdio startproc oninit
REGRESS = init base types numspecial bytea composite encoding datetime jsonb misc variadic shared sd trigger trigger2 spi raise errors sqlstate errcontext cargs pseudo anycompat srf out varnames validator replace classes prepare cursor hostile compat txn evttrig subxact modules quote require cookbook stdio startproc oninit

PG_CONFIG ?= pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
Expand Down
1 change: 1 addition & 0 deletions doc/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Legend: ✅ supported · ➖ not applicable / different mechanism · ❌ not pro
| Procedures + transaction control | ✅ `spi_commit`/`spi_rollback` | ✅ | ✅ | ✅ `commit`/`rollback` |
| Explicit subtransactions | ✅ `subtransaction { }` | ✅ `subtransaction(callable)` | ➖ (`eval`/`spi_exec` traps) | ✅ `subtransaction { }` |
| Session-shared data | ✅ `$_SHARED` | ✅ `$_SHARED` | ✅ `%_SHARED` | ✅ `GD` |
| Per-function static data | ✅ `$_SD` | ➖ | ➖ | ➖ |
| Module autoloading | ✅ `plruby_modules` | ✅ `plphp_modules` | ➖ | ✅ `pltcl_modules` + `unknown` |
| Start proc | ✅ `plruby.start_proc` | ✅ `plphp.start_proc` | ✅ `plperl.on_init` etc. | ✅ `pltcl.start_proc` |
| Standard output forwarded to server log | ✅ `puts`/`print` | ✅ | ➖ (`elog`) | ➖ (`elog`) |
Expand Down
19 changes: 19 additions & 0 deletions doc/plruby.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ non-thread-safe embedding).
- [Quoting helpers](#quoting-helpers)
- [Messaging: elog and pg_raise](#messaging-elog-and-pg_raise)
- [Shared data: `$_SHARED`](#shared-data-_shared)
- [Per-function data: `$_SD`](#per-function-data-_sd)
- [Session initialization: on_init, modules and start_proc](#session-initialization-on_init-modules-and-start_proc)
- [Errors and exceptions](#errors-and-exceptions)
- [Security](#security)
Expand Down Expand Up @@ -505,6 +506,24 @@ CREATE FUNCTION get_shared(key text) RETURNS text LANGUAGE plruby AS $$
$$;
```

## Per-function data: `$_SD`

`$_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:

```sql
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.

## Session initialization: on_init, modules and start_proc

Three mechanisms let you run Ruby setup code the first time PL/Ruby is used in a
Expand Down
87 changes: 87 additions & 0 deletions expected/sd.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
--
-- $_SD: a Hash private to each function, persisting across calls to that
-- function within a session (counterpart of PL/Python's SD; $_SHARED is GD).
--
-- Persists across calls to the same function.
CREATE FUNCTION sd_counter() RETURNS int LANGUAGE plruby AS $$
$_SD['n'] = ($_SD['n'] || 0) + 1
$$;
SELECT sd_counter();
sd_counter
------------
1
(1 row)

SELECT sd_counter();
sd_counter
------------
2
(1 row)

SELECT sd_counter();
sd_counter
------------
3
(1 row)

-- A different function has its own independent $_SD.
CREATE FUNCTION sd_counter2() RETURNS int LANGUAGE plruby AS $$
$_SD['n'] = ($_SD['n'] || 0) + 1
$$;
SELECT sd_counter2();
sd_counter2
-------------
1
(1 row)

SELECT sd_counter(); -- unaffected by sd_counter2, keeps counting
sd_counter
------------
4
(1 row)

-- $_SD is distinct from the session-global $_SHARED.
CREATE FUNCTION sd_vs_shared() RETURNS text LANGUAGE plruby AS $$
$_SD['who'] = 'sd'
$_SHARED['who'] = 'shared'
"sd=#{$_SD['who']} shared=#{$_SHARED['who']}"
$$;
SELECT sd_vs_shared();
sd_vs_shared
---------------------
sd=sd shared=shared
(1 row)

-- CREATE OR REPLACE resets $_SD (it is tied to the compiled function).
SELECT sd_counter(); -- still climbing before replace
sd_counter
------------
5
(1 row)

CREATE OR REPLACE FUNCTION sd_counter() RETURNS int LANGUAGE plruby AS $$
$_SD['n'] = ($_SD['n'] || 0) + 1
$$;
SELECT sd_counter(); -- back to 1 after replace
sd_counter
------------
1
(1 row)

-- $_SD survives even when a later call in the same function raises.
CREATE FUNCTION sd_stash_then_fail() RETURNS int LANGUAGE plruby AS $$
$_SD['kept'] = 'yes'
raise 'deliberate failure'
$$;
SELECT sd_stash_then_fail();
ERROR: deliberate failure
CONTEXT: PL/Ruby function "sd_stash_then_fail"
CREATE FUNCTION sd_read_kept() RETURNS text LANGUAGE plruby AS $$
$_SD['kept'] || 'unset'
$$;
SELECT sd_read_kept(); -- its own $_SD: never set 'kept'
sd_read_kept
--------------
unset
(1 row)

56 changes: 56 additions & 0 deletions plruby.c
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ static char *plruby_start_proc = NULL;
static char *plruby_on_init = NULL;
static bool plruby_session_inited = false;

/*
* Per-function static storage. Maps a function's internal proc name (see
* plruby_compile_function) to a Hash that is exposed to the body as $_SD and
* persists across calls to that function within the session. Counterpart of
* PL/Python's SD; $_SHARED is the session-global GD. Entries are dropped when
* a function is recompiled, so $_SD resets on CREATE OR REPLACE.
*/
static VALUE plruby_sd_table;

/* Forward declarations */
void _PG_init(void);
void plruby_init(void);
Expand Down Expand Up @@ -374,6 +383,10 @@ plruby_init(void)
plruby_proc_cache = rb_hash_new();
rb_gc_register_address(&plruby_proc_cache);

plruby_sd_table = rb_hash_new();
rb_gc_register_address(&plruby_sd_table);
rb_gv_set("$_SD", rb_hash_new()); /* placeholder until a body binds one */

rb_gc_register_address(&current_srf_binding);

plruby_spi_init();
Expand Down Expand Up @@ -504,6 +517,29 @@ plruby_error_callback(void *arg)
* Call handler
* ------------------------------------------------------------------- */

/*
* Bind $_SD to this function's private, session-persistent Hash and return the
* previous $_SD so the caller can restore it (needed when a body re-enters
* PL/Ruby via SPI). The hash is keyed by the internal proc name, so it is
* distinct for every function -- and for a function's trigger vs plain form --
* and is dropped when the function is recompiled.
*/
static VALUE
plruby_bind_sd(plruby_proc_desc *desc)
{
VALUE prev = rb_gv_get("$_SD");
VALUE key = rb_str_new_cstr(desc->proname);
VALUE sd = rb_hash_aref(plruby_sd_table, key);

if (NIL_P(sd))
{
sd = rb_hash_new();
rb_hash_aset(plruby_sd_table, key, sd);
}
rb_gv_set("$_SD", sd);
return prev;
}

Datum
plruby_call_handler(PG_FUNCTION_ARGS)
{
Expand Down Expand Up @@ -537,6 +573,7 @@ plruby_call_handler(PG_FUNCTION_ARGS)
int save_call_ntrf = plruby_call_ntransforms;
plruby_transform_info *save_arg_trf = plruby_arg_transforms;
int save_arg_ntrf = plruby_arg_ntransforms;
VALUE save_sd = rb_gv_get("$_SD");
ErrorContextCallback ecb;
plruby_error_ctx errctx;

Expand Down Expand Up @@ -564,20 +601,23 @@ plruby_call_handler(PG_FUNCTION_ARGS)
desc = plruby_compile_function(fcinfo->flinfo->fn_oid, true, false);
plruby_call_transforms = desc->transforms;
plruby_call_ntransforms = desc->n_transforms;
plruby_bind_sd(desc);
retval = plruby_trigger_handler(fcinfo, desc);
}
else if (CALLED_AS_EVENT_TRIGGER(fcinfo))
{
desc = plruby_compile_function(fcinfo->flinfo->fn_oid, false, true);
plruby_call_transforms = NULL;
plruby_call_ntransforms = 0;
plruby_bind_sd(desc);
retval = plruby_event_trigger_handler(fcinfo, desc);
}
else
{
desc = plruby_compile_function(fcinfo->flinfo->fn_oid, false, false);
plruby_call_transforms = desc->transforms;
plruby_call_ntransforms = desc->n_transforms;
plruby_bind_sd(desc);
if (desc->retset)
retval = plruby_srf_handler(fcinfo, desc);
else
Expand All @@ -591,6 +631,7 @@ plruby_call_handler(PG_FUNCTION_ARGS)
plruby_call_ntransforms = save_call_ntrf;
plruby_arg_transforms = save_arg_trf;
plruby_arg_ntransforms = save_arg_ntrf;
rb_gv_set("$_SD", save_sd);
PG_RE_THROW();
}
PG_END_TRY();
Expand All @@ -600,6 +641,7 @@ plruby_call_handler(PG_FUNCTION_ARGS)
plruby_call_ntransforms = save_call_ntrf;
plruby_arg_transforms = save_arg_trf;
plruby_arg_ntransforms = save_arg_ntrf;
rb_gv_set("$_SD", save_sd);
}

return retval;
Expand Down Expand Up @@ -1257,6 +1299,10 @@ plruby_compile_function(Oid fnoid, bool is_trigger, bool is_event_trigger)
*/
rb_hash_delete(plruby_proc_cache,
rb_str_new_cstr(internal_proname));

/* Reset $_SD too, so a recompile starts with empty storage. */
rb_hash_delete(plruby_sd_table,
rb_str_new_cstr(internal_proname));
}
}
}
Expand Down Expand Up @@ -1715,6 +1761,7 @@ plruby_inline_handler(PG_FUNCTION_ARGS)
StringInfoData src;
int state = 0;
VALUE params[2];
VALUE save_sd;
ErrorContextCallback ecb;
plruby_error_ctx errctx;

Expand Down Expand Up @@ -1759,7 +1806,16 @@ plruby_inline_handler(PG_FUNCTION_ARGS)

params[0] = rb_ary_new();
params[1] = INT2NUM(0);

/*
* An anonymous block has no persistent identity, so $_SD is a fresh empty
* Hash for the duration of the block (matching PL/Python, where a DO block
* gets its own SD that does not survive). $_SHARED still persists.
*/
save_sd = rb_gv_get("$_SD");
rb_gv_set("$_SD", rb_hash_new());
plruby_protected_call(plruby_recv, rb_intern(funcname), 2, params);
rb_gv_set("$_SD", save_sd);

plruby_remove_method(funcname);

Expand Down
45 changes: 45 additions & 0 deletions sql/sd.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
--
-- $_SD: a Hash private to each function, persisting across calls to that
-- function within a session (counterpart of PL/Python's SD; $_SHARED is GD).
--

-- Persists across calls to the same function.
CREATE FUNCTION sd_counter() RETURNS int LANGUAGE plruby AS $$
$_SD['n'] = ($_SD['n'] || 0) + 1
$$;
SELECT sd_counter();
SELECT sd_counter();
SELECT sd_counter();

-- A different function has its own independent $_SD.
CREATE FUNCTION sd_counter2() RETURNS int LANGUAGE plruby AS $$
$_SD['n'] = ($_SD['n'] || 0) + 1
$$;
SELECT sd_counter2();
SELECT sd_counter(); -- unaffected by sd_counter2, keeps counting

-- $_SD is distinct from the session-global $_SHARED.
CREATE FUNCTION sd_vs_shared() RETURNS text LANGUAGE plruby AS $$
$_SD['who'] = 'sd'
$_SHARED['who'] = 'shared'
"sd=#{$_SD['who']} shared=#{$_SHARED['who']}"
$$;
SELECT sd_vs_shared();

-- CREATE OR REPLACE resets $_SD (it is tied to the compiled function).
SELECT sd_counter(); -- still climbing before replace
CREATE OR REPLACE FUNCTION sd_counter() RETURNS int LANGUAGE plruby AS $$
$_SD['n'] = ($_SD['n'] || 0) + 1
$$;
SELECT sd_counter(); -- back to 1 after replace

-- $_SD survives even when a later call in the same function raises.
CREATE FUNCTION sd_stash_then_fail() RETURNS int LANGUAGE plruby AS $$
$_SD['kept'] = 'yes'
raise 'deliberate failure'
$$;
SELECT sd_stash_then_fail();
CREATE FUNCTION sd_read_kept() RETURNS text LANGUAGE plruby AS $$
$_SD['kept'] || 'unset'
$$;
SELECT sd_read_kept(); -- its own $_SD: never set 'kept'
Loading