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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ jobs:
sudo make -C jsonb_plruby PG_CONFIG="$PG_CONFIG" RUBY="$RUBY" install
make -C hstore_plruby PG_CONFIG="$PG_CONFIG" RUBY="$RUBY"
sudo make -C hstore_plruby PG_CONFIG="$PG_CONFIG" RUBY="$RUBY" install
make -C ltree_plruby PG_CONFIG="$PG_CONFIG" RUBY="$RUBY"
sudo make -C ltree_plruby PG_CONFIG="$PG_CONFIG" RUBY="$RUBY" install

- name: Start a test cluster
run: |
Expand All @@ -79,6 +81,12 @@ jobs:
env:
PGPORT: '5555'

- name: Run the ltree_plruby regression suite
run: |
make -C ltree_plruby PG_CONFIG=/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config installcheck
env:
PGPORT: '5555'

- name: Show regression diffs on failure
if: failure()
run: |
Expand All @@ -88,3 +96,5 @@ jobs:
cat jsonb_plruby/regression.diffs 2>/dev/null || echo "(none)"
echo "===== hstore_plruby regression.diffs ====="
cat hstore_plruby/regression.diffs 2>/dev/null || echo "(none)"
echo "===== ltree_plruby regression.diffs ====="
cat ltree_plruby/regression.diffs 2>/dev/null || echo "(none)"
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ and the project aims to follow [Semantic Versioning](https://semver.org/).

### Added

- **`ltree_plruby`**: a companion extension (in `ltree_plruby/`) providing
`TRANSFORM FOR TYPE ltree`: opted-in functions receive ltree arguments as a
Ruby `Array` of label Strings and may return an `Array` of labels into an
ltree result (elements stringified and joined with `.`; the empty ltree is the
empty `Array`). Mirrors the in-core `ltree_plpython` transform. Implemented
against ltree's public `ltree2text`/`text2ltree` functions, so it needs no
ltree headers and works with the packaged ltree on PostgreSQL 11-18.
- **`$_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
Expand Down
16 changes: 9 additions & 7 deletions INSTALL
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,21 @@ README for why.
make installcheck


5. The jsonb and hstore transforms (optional)
---------------------------------------------
5. The jsonb, hstore, and ltree transforms (optional)
-----------------------------------------------------

The jsonb_plruby/ and hstore_plruby/ subdirectories each build a separate
extension providing TRANSFORM FOR TYPE jsonb (native Ruby Hashes/Arrays)
and TRANSFORM FOR TYPE hstore (Ruby Hash of String => String/nil):
The jsonb_plruby/, hstore_plruby/, and ltree_plruby/ subdirectories each
build a separate extension providing TRANSFORM FOR TYPE jsonb (native Ruby
Hashes/Arrays), TRANSFORM FOR TYPE hstore (Ruby Hash of String => String/nil),
and TRANSFORM FOR TYPE ltree (Ruby Array of label Strings):

cd jsonb_plruby # or hstore_plruby
cd jsonb_plruby # or hstore_plruby, or ltree_plruby
make
sudo make install
# in a database that has (or will get) plruby:
# CREATE EXTENSION jsonb_plruby CASCADE;
# CREATE EXTENSION hstore_plruby CASCADE; -- also requires hstore
# CREATE EXTENSION ltree_plruby CASCADE; -- also requires ltree

Both accept the same PG_CONFIG/RUBY overrides and have their own
All three accept the same PG_CONFIG/RUBY overrides and have their own
"make installcheck".
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ SELECT hello('world'); -- Hello, world!
| 🔐 **Transaction control** | `spi_commit` / `spi_rollback` in procedures, plus `subtransaction` blocks. |
| 🧰 **Utilities** | `quote_literal` / `quote_nullable` / `quote_ident`, `elog`, `$_SHARED`. |
| 📦 **Session setup** | Anonymous `DO` blocks, `plruby_modules` autoloading, and a `plruby.start_proc` hook. |
| 🔄 **Transforms** | `jsonb_plruby` and `hstore_plruby`: functions declared `TRANSFORM FOR TYPE` exchange native Ruby Hashes/Arrays with `jsonb` and `hstore`. |
| 🔄 **Transforms** | `jsonb_plruby`, `hstore_plruby`, and `ltree_plruby`: functions declared `TRANSFORM FOR TYPE` exchange native Ruby Hashes/Arrays with `jsonb`, `hstore`, and `ltree`. |

See the [**language reference**](doc/plruby.md) for the full API, the
[**cookbook**](doc/cookbook.md) for tested recipes, and the
Expand Down
20 changes: 20 additions & 0 deletions doc/plruby.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,26 @@ 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.

```sql
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
$$;
```

## Arguments

PL/Ruby supports the full range of argument modes.
Expand Down
29 changes: 29 additions & 0 deletions ltree_plruby/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# ltree_plruby - transform between ltree and Ruby Arrays for PL/Ruby
#
# Build after (or alongside) the plruby extension in the parent directory;
# the transform requires the ltree and plruby extensions at CREATE
# EXTENSION time.

MODULE_big = ltree_plruby
OBJS = ltree_plruby.o

EXTENSION = ltree_plruby
DATA = ltree_plruby--1.0.sql

# Ruby compile/link flags, discovered via RbConfig (same as ../Makefile).
RUBY ?= ruby
RUBY_ARCHHDRDIR := $(shell $(RUBY) -rrbconfig -e 'print RbConfig::CONFIG["rubyarchhdrdir"]')
RUBY_HDRDIR := $(shell $(RUBY) -rrbconfig -e 'print RbConfig::CONFIG["rubyhdrdir"]')
RUBY_LIBDIR := $(shell $(RUBY) -rrbconfig -e 'print RbConfig::CONFIG["libdir"]')
RUBY_ARCHLIBDIR := $(shell $(RUBY) -rrbconfig -e 'print RbConfig::CONFIG["archlibdir"]')
RUBY_LIBARG := $(shell $(RUBY) -rrbconfig -e 'print RbConfig::CONFIG["LIBRUBYARG_SHARED"]')
RUBY_LIBS := $(shell $(RUBY) -rrbconfig -e 'print RbConfig::CONFIG["LIBS"]')

PG_CPPFLAGS = -I$(RUBY_ARCHHDRDIR) -I$(RUBY_HDRDIR)
SHLIB_LINK = -L$(RUBY_LIBDIR) -L$(RUBY_ARCHLIBDIR) $(RUBY_LIBARG) $(RUBY_LIBS)

REGRESS = ltree_plruby

PG_CONFIG ?= pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)
166 changes: 166 additions & 0 deletions ltree_plruby/expected/ltree_plruby.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
--
-- ltree <-> Ruby Array transform. Functions must opt in with
-- TRANSFORM FOR TYPE ltree. An ltree arrives as an Array of its label
-- Strings, and an Array of labels is joined back into an ltree.
--
CREATE EXTENSION ltree_plruby CASCADE;
NOTICE: installing required extension "ltree"
NOTICE: installing required extension "plruby"
-- An ltree argument arrives as an Array of label Strings.
CREATE FUNCTION lt_labels(ltree) RETURNS text
TRANSFORM FOR TYPE ltree
LANGUAGE plruby AS $$
"#{args[0].class.name}: #{args[0].inspect}"
$$;
SELECT lt_labels('Top.Science.Astronomy'::ltree);
lt_labels
----------------------------------------
Array: ["Top", "Science", "Astronomy"]
(1 row)

-- Array operations map onto path operations.
CREATE FUNCTION lt_nlabels(ltree) RETURNS int
TRANSFORM FOR TYPE ltree
LANGUAGE plruby AS $$
args[0].length
$$;
SELECT lt_nlabels('a.b.c'::ltree);
lt_nlabels
------------
3
(1 row)

SELECT lt_nlabels(''::ltree);
lt_nlabels
------------
0
(1 row)

CREATE FUNCTION lt_parent(ltree) RETURNS ltree
TRANSFORM FOR TYPE ltree
LANGUAGE plruby AS $$
args[0][0...-1]
$$;
SELECT lt_parent('a.b.c'::ltree);
lt_parent
-----------
a.b
(1 row)

CREATE FUNCTION lt_reverse(ltree) RETURNS ltree
TRANSFORM FOR TYPE ltree
LANGUAGE plruby AS $$
args[0].reverse
$$;
SELECT lt_reverse('a.b.c'::ltree);
lt_reverse
------------
c.b.a
(1 row)

-- Build an ltree from Ruby data: elements are stringified before joining.
CREATE FUNCTION lt_build(int) RETURNS ltree
TRANSFORM FOR TYPE ltree
LANGUAGE plruby AS $$
['node', args[0], args[0] * 2]
$$;
SELECT lt_build(3);
lt_build
----------
node.3.6
(1 row)

-- The empty ltree round-trips as the empty Array.
CREATE FUNCTION lt_echo(ltree) RETURNS ltree
TRANSFORM FOR TYPE ltree
LANGUAGE plruby AS $$
args[0]
$$;
SELECT lt_echo(''::ltree) = ''::ltree AS empty_roundtrip;
empty_roundtrip
-----------------
t
(1 row)

-- A NULL ltree argument is nil; returning nil is SQL NULL.
SELECT lt_echo(NULL) IS NULL AS null_roundtrip;
null_roundtrip
----------------
t
(1 row)

-- Returning something other than an Array is rejected cleanly.
CREATE FUNCTION lt_bad() RETURNS ltree
TRANSFORM FOR TYPE ltree
LANGUAGE plruby AS $$
'not an array'
$$;
SELECT lt_bad();
ERROR: cannot transform Ruby object of class String to ltree
HINT: An ltree value is built from an Array of labels.
CONTEXT: PL/Ruby function "lt_bad"
-- The transform reaches nested contexts: SETOF rows via return_next...
CREATE FUNCTION lt_ancestors(ltree) RETURNS SETOF ltree
TRANSFORM FOR TYPE ltree
LANGUAGE plruby AS $$
labels = args[0]
(1..labels.length).each { |n| return_next(labels[0...n]) }
nil
$$;
SELECT * FROM lt_ancestors('a.b.c'::ltree);
lt_ancestors
--------------
a
a.b
a.b.c
(3 rows)

-- ...and composite rows with an ltree column.
CREATE FUNCTION lt_rows(ltree) RETURNS TABLE (depth int, path ltree)
TRANSFORM FOR TYPE ltree
LANGUAGE plruby AS $$
labels = args[0]
(1..labels.length).each do |n|
return_next({'depth' => n, 'path' => labels[0...n]})
end
nil
$$;
SELECT * FROM lt_rows('x.y'::ltree);
depth | path
-------+------
1 | x
2 | x.y
(2 rows)

-- Triggers that declare the transform get $_TD ltree columns as Arrays,
-- and 'MODIFY' accepts an Array back.
CREATE TABLE lt_items (id int, path ltree);
CREATE FUNCTION lt_normalize() RETURNS trigger
TRANSFORM FOR TYPE ltree
LANGUAGE plruby AS $$
p = $_TD['new']['path']
elog('NOTICE', "path is a #{p.class.name}: #{p.inspect}")
$_TD['new']['path'] = p.map(&:downcase)
'MODIFY'
$$;
CREATE TRIGGER lt_items_norm BEFORE INSERT ON lt_items
FOR EACH ROW EXECUTE PROCEDURE lt_normalize();
INSERT INTO lt_items VALUES (1, 'Top.Science');
NOTICE: path is a Array: ["Top", "Science"]
SELECT id, path FROM lt_items;
id | path
----+-------------
1 | top.science
(1 row)

DROP TABLE lt_items;
-- Without the TRANSFORM clause, ltree still travels as a String.
CREATE FUNCTION lt_untransformed(ltree) RETURNS text LANGUAGE plruby AS $$
"#{args[0].class.name}: #{args[0]}"
$$;
SELECT lt_untransformed('a.b'::ltree);
lt_untransformed
------------------
String: a.b
(1 row)

27 changes: 27 additions & 0 deletions ltree_plruby/ltree_plruby--1.0.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* Transform between ltree and Ruby Arrays for PL/Ruby.
*
* With this transform, a function created with TRANSFORM FOR TYPE ltree
* receives ltree arguments as a Ruby Array of label Strings, and may return
* an Array of labels into an ltree result.
*/

-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "CREATE EXTENSION ltree_plruby" to load this file. \quit

CREATE FUNCTION ltree_to_plruby(val internal)
RETURNS internal
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT IMMUTABLE;

CREATE FUNCTION plruby_to_ltree(val internal)
RETURNS ltree
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT IMMUTABLE;

CREATE TRANSFORM FOR ltree LANGUAGE plruby (
FROM SQL WITH FUNCTION ltree_to_plruby(internal),
TO SQL WITH FUNCTION plruby_to_ltree(internal)
);

COMMENT ON TRANSFORM FOR ltree LANGUAGE plruby
IS 'transform between ltree and Ruby Arrays';
Loading
Loading