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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ and the project aims to follow [Semantic Versioning](https://semver.org/).
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.
- **SPI result column metadata.** `spi_colnames(result)`, `spi_coltypes(result)`,
and `spi_coltypmods(result)` return parallel `Array`s of a result's column
names, type OIDs, and type modifiers, the counterparts of PL/Python's
`colnames` / `coltypes` / `coltypmods`. A non-`SELECT` result returns empty
`Array`s.

### Changed

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 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
REGRESS = init base types numspecial bytea composite encoding datetime jsonb misc variadic shared sd trigger trigger2 spi colmeta 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 @@ -37,6 +37,7 @@ Legend: ✅ supported · ➖ not applicable / different mechanism · ❌ not pro
| Iterate rows | `spi_fetch_row` to `Hash`/`nil` | `spi_fetch_row` | `spi_fetchrow` | row loop / `-array` |
| Stream via cursor | `spi_query`(+block)/`spi_fetchrow`/`spi_cursor_close`, `Cursor#each` | ➖ | `spi_query`/`spi_fetchrow`/`spi_cursor_close` | `spi_exec`/cursors |
| Rows processed / status / rewind | `spi_processed`/`spi_status`/`spi_rewind` | ✅ same | ➖ (return hash) | ➖ |
| Result column metadata | `spi_colnames`/`spi_coltypes`/`spi_coltypmods` | ➖ | ➖ | ➖ |
| Prepare a plan | `spi_prepare(q, types...)` | ✅ same | `spi_prepare` | `spi_prepare` |
| Execute a plan | `spi_exec_prepared` / `spi_query_prepared` | ✅ same | `spi_exec_prepared`/`spi_query_prepared` | `spi_execp` |
| Free a plan | `spi_freeplan` | ✅ same | `spi_freeplan` | (auto) |
Expand Down
10 changes: 10 additions & 0 deletions doc/plruby.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,8 +339,18 @@ Run queries against the current database from within a function:
rows are exhausted.
- `spi_processed(result)`: number of rows the query produced.
- `spi_status(result)`: the SPI status code as a `String`.
- `spi_colnames(result)`: an `Array` of the result's column names (`String`s).
- `spi_coltypes(result)`: an `Array` of the columns' type OIDs (`Integer`s);
cast one to a name with `oid::regtype` in SQL.
- `spi_coltypmods(result)`: an `Array` of the columns' type modifiers
(`Integer`s), e.g. `14` for `varchar(10)`, or `-1` when 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 `Array`s 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`.

```sql
CREATE FUNCTION sum_series(n integer) RETURNS integer LANGUAGE plruby AS $$
r = spi_exec("select generate_series(1, #{args[0]}) as g")
Expand Down
47 changes: 47 additions & 0 deletions expected/colmeta.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
--
-- SPI result column metadata: spi_colnames / spi_coltypes / spi_coltypmods,
-- the counterparts of PL/Python's colnames / coltypes / coltypmods. Each is a
-- parallel Array over the result's columns; type OIDs are Integers.
--
-- A SELECT result exposes its column names, type OIDs, and type modifiers.
-- (int4 = 23, text = 25, varchar = 1043; varchar(10) typmod = 14 = 10 + 4.)
CREATE FUNCTION colmeta() RETURNS text LANGUAGE plruby AS $$
r = spi_exec("SELECT 1::int AS id, 'hi'::text AS label, 'ab'::varchar(10) AS code")
"names=#{spi_colnames(r).inspect} " +
"types=#{spi_coltypes(r).inspect} " +
"typmods=#{spi_coltypmods(r).inspect}"
$$;
SELECT colmeta();
colmeta
-------------------------------------------------------------------------
names=["id", "label", "code"] types=[23, 25, 1043] typmods=[-1, -1, 14]
(1 row)

-- The OIDs are usable: map them back to type names through the catalog.
CREATE FUNCTION colmeta_typenames() RETURNS text[] LANGUAGE plruby AS $$
r = spi_exec("SELECT 1::int AS id, 'ab'::varchar(10) AS code")
spi_coltypes(r).map do |oid|
t = spi_exec("SELECT #{oid}::regtype::text AS n")
spi_fetch_row(t)['n']
end
$$;
SELECT colmeta_typenames();
colmeta_typenames
-------------------------------
{integer,"character varying"}
(1 row)

-- A non-SELECT (no tuple table) has no columns: each metadata Array is empty.
CREATE FUNCTION colmeta_ddl() RETURNS text LANGUAGE plruby AS $$
spi_exec("CREATE TEMP TABLE t_cm(x int)")
r = spi_exec("INSERT INTO t_cm VALUES (1)")
"names=#{spi_colnames(r).inspect} " +
"types=#{spi_coltypes(r).inspect} " +
"processed=#{spi_processed(r)}"
$$;
SELECT colmeta_ddl();
colmeta_ddl
-------------------------------
names=[] types=[] processed=1
(1 row)

72 changes: 72 additions & 0 deletions plruby_spi.c
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,72 @@ plruby_spi_status(VALUE self, VALUE res)
return rb_str_new_cstr(SPI_result_code_string(r->status));
}

/*
* Column metadata for a result's tuple descriptor, as parallel Arrays over the
* (non-dropped) result columns -- the counterparts of PL/Python's colnames /
* coltypes / coltypmods. A result without a tuple table (a non-SELECT, like a
* plain INSERT) has no columns, so each returns an empty Array.
*/
typedef enum
{
PLRUBY_COL_NAME,
PLRUBY_COL_TYPE,
PLRUBY_COL_TYPMOD
} plruby_col_kind;

static VALUE
plruby_spi_colmeta(VALUE res, plruby_col_kind kind)
{
plruby_spi_result *r = plruby_get_spi_result(res);
VALUE out = rb_ary_new();
TupleDesc tupdesc;
int i;

if (r->tuptable == NULL)
return out;

tupdesc = r->tuptable->tupdesc;
for (i = 0; i < tupdesc->natts; i++)
{
Form_pg_attribute att = TupleDescAttr(tupdesc, i);

if (att->attisdropped)
continue;

switch (kind)
{
case PLRUBY_COL_NAME:
rb_ary_push(out, rb_str_new_cstr(NameStr(att->attname)));
break;
case PLRUBY_COL_TYPE:
rb_ary_push(out, UINT2NUM(att->atttypid));
break;
case PLRUBY_COL_TYPMOD:
rb_ary_push(out, INT2NUM(att->atttypmod));
break;
}
}
return out;
}

static VALUE
plruby_spi_colnames(VALUE self, VALUE res)
{
return plruby_spi_colmeta(res, PLRUBY_COL_NAME);
}

static VALUE
plruby_spi_coltypes(VALUE self, VALUE res)
{
return plruby_spi_colmeta(res, PLRUBY_COL_TYPE);
}

static VALUE
plruby_spi_coltypmods(VALUE self, VALUE res)
{
return plruby_spi_colmeta(res, PLRUBY_COL_TYPMOD);
}

static VALUE
plruby_spi_rewind(VALUE self, VALUE res)
{
Expand Down Expand Up @@ -1277,6 +1343,12 @@ plruby_spi_init(void)
RUBY_METHOD_FUNC(plruby_spi_processed), 1);
rb_define_global_function("spi_status",
RUBY_METHOD_FUNC(plruby_spi_status), 1);
rb_define_global_function("spi_colnames",
RUBY_METHOD_FUNC(plruby_spi_colnames), 1);
rb_define_global_function("spi_coltypes",
RUBY_METHOD_FUNC(plruby_spi_coltypes), 1);
rb_define_global_function("spi_coltypmods",
RUBY_METHOD_FUNC(plruby_spi_coltypmods), 1);
rb_define_global_function("spi_rewind",
RUBY_METHOD_FUNC(plruby_spi_rewind), 1);

Expand Down
35 changes: 35 additions & 0 deletions sql/colmeta.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
--
-- SPI result column metadata: spi_colnames / spi_coltypes / spi_coltypmods,
-- the counterparts of PL/Python's colnames / coltypes / coltypmods. Each is a
-- parallel Array over the result's columns; type OIDs are Integers.
--

-- A SELECT result exposes its column names, type OIDs, and type modifiers.
-- (int4 = 23, text = 25, varchar = 1043; varchar(10) typmod = 14 = 10 + 4.)
CREATE FUNCTION colmeta() RETURNS text LANGUAGE plruby AS $$
r = spi_exec("SELECT 1::int AS id, 'hi'::text AS label, 'ab'::varchar(10) AS code")
"names=#{spi_colnames(r).inspect} " +
"types=#{spi_coltypes(r).inspect} " +
"typmods=#{spi_coltypmods(r).inspect}"
$$;
SELECT colmeta();

-- The OIDs are usable: map them back to type names through the catalog.
CREATE FUNCTION colmeta_typenames() RETURNS text[] LANGUAGE plruby AS $$
r = spi_exec("SELECT 1::int AS id, 'ab'::varchar(10) AS code")
spi_coltypes(r).map do |oid|
t = spi_exec("SELECT #{oid}::regtype::text AS n")
spi_fetch_row(t)['n']
end
$$;
SELECT colmeta_typenames();

-- A non-SELECT (no tuple table) has no columns: each metadata Array is empty.
CREATE FUNCTION colmeta_ddl() RETURNS text LANGUAGE plruby AS $$
spi_exec("CREATE TEMP TABLE t_cm(x int)")
r = spi_exec("INSERT INTO t_cm VALUES (1)")
"names=#{spi_colnames(r).inspect} " +
"types=#{spi_coltypes(r).inspect} " +
"processed=#{spi_processed(r)}"
$$;
SELECT colmeta_ddl();
Loading