-- Complain if script is sourced in psql, rather than via CREATE EXTENSION. \echo Use "CREATE EXTENSION pg_rowalesce" to load this file. \quit -------------------------------------------------------------------------------------------------------------- comment on extension pg_rowalesce is $markdown$ # The `pg_rowalesce` PostgreSQL extension The `pg_rowalesce` PostgreSQL extension its defining feature is the `rowalesce()` function. `rowalesce()` is like `coalesce()`, but for rows and other composite types. From its arbitrary number of argument rows, for each field/column, `rowalesce()` takes the value from the first row for which that particular field/column has a `not null` value. `rowalesce()` comes in a number of variants: 1. `rowalesce(variadic anyarray, out anyelement)` 2. `rowalesce(in jsonb, variadic anyarray, out anyelement)` 3. `rowalesce(in record, variadic anyarray, out anyelement)` 4. `rowalesce(anyelement, jsonb, out anyelement)` These variants make it easy to combine data from different sources, as long as there is at least one argument to mark the type proper. A properly composite-typed `NULL` argument can be used to just force the correct row type, as in: ```sql select rowalesce('{"my_attr_1": 3, "my_attr_2": "b"}'::jsonb, null::my.type) ``` Besides these variations, there is also a `rowalesce_with_defaults()` variant of the first 3 of those, plus one extra, to work with the so very loose `record` type: 1. `rowalesce_with_defaults(variadic anyarray, out anyelement)` 2. `rowalesce_with_defaults(in jsonb, variadic anyarray, out anyelement)` 3. `rowalesce_with_defaults(in hstore, variadic anyarray, out anyelement)` 4. `rowalesce_with_defaults(in record, variadic anyarray, out anyelement)` `rowalesce_with_defaults()` depends on `table_defaults()`, which can also be used separately, if you wish to evaluate all of a table its default expressions (or a subset thereof) for some other purpose. Finally, there is the `insert_row()` function which makes inserting the result of these functions easier. ## Dependencies This extension only depends on the `hstore` extension. There _are_ extensions which will _enhance_ `pg_rowalesce`, but these are not necessary for its proper functioning. ## Installation Installation is done by means of a `Makefile`, which depends on the [PGXS](https://www.postgresql.org/docs/current/extend-pgxs.html) infrastructure that should come as part of your PostgreSQL installation. ```bash make install ``` Installing a PostgreSQL extension successfully requires access to the `$(pg_config --sharedir)/extension` directory. After the extension files have been installed by `make install`, as usual, the extension can be installes by means of: ```sql CREATE EXTENSION pg_rowalesce; ``` `pg_rowalesce` supports the `WITH SCHEMA` option of the [`CREATE EXTENSION`](https://www.postgresql.org/docs/current/sql-createextension.html) command. ## Schema relocation `pg_rowalesce` supports schema relocation, _but_… There is one manual step involved if you want to make it work extra super-duper well: you have to call the `pg_rowalesce_relocate(name)` function, either _instead_ of `ALTER EXTENSION pg_rowalesce SET SCHEMA _new_schema_`, as ```sql SELECT pg_rowalesce_relocate('new_schema'); ``` Or _after_ `ALTER EXTENSION pg_rowalesce SET SCHEMA _new_schema_` (in which case the name of the new schema doesn't need to be supplied): ```sql ALTER EXTENSION pg_rowalesce SET SCHEMA new_schema; SELECT pg_rowalesce_relocate(); ``` ## Extension object reference ## Colophon $markdown$; -------------------------------------------------------------------------------------------------------------- do $$ declare _ddl_cmd_to_set_pg_readme_url text; begin _ddl_cmd_to_set_pg_readme_url := 'ALTER DATABASE ' || current_database() || ' SET pg_rowalesce.readme_url = ''https://github.com/bigsmoke/pg_rowalesce/blob/master/README.md'''; execute _ddl_cmd_to_set_pg_readme_url; exception when insufficient_privilege then -- We say `superuser = false` in the control file; so let's just whine a little instead of crashing. raise warning using message = format( 'Because you''re installing the pg_rowalesce extension as non-superuser and because you' || ' are also not the owner of the %I DB, the database-level `pg_rowalesce.readme_url`' || ' setting has not been set.', current_database() ) ,detail = 'Settings of the form `.readme_url` are used by the `pg_readme`' || ' extension to cross-link between extensions their README files.' ,hint = 'If you want full inter-extension README cross-linking, you can ask your friendly' || E' neighbourhood DBA to execute the following statement:\n' || _ddl_cmd_to_set_pg_readme_url || ';'; end; $$; -------------------------------------------------------------------------------------------------------------- create function pg_rowalesce_readme() returns text volatile set search_path from current set pg_readme.include_view_definitions to 'true' set pg_readme.include_routine_definitions_like to '{test__%}' language plpgsql as $plpgsql$ declare _readme text; begin create extension if not exists pg_readme with cascade; _readme := pg_extension_readme('pg_rowalesce'::name); raise transaction_rollback; -- to drop extension if we happened to `CREATE EXTENSION` for just this. exception when transaction_rollback then return _readme; end; $plpgsql$; -------------------------------------------------------------------------------------------------------------- create function pg_rowalesce_meta_pgxn() returns jsonb stable set search_path from current language sql return jsonb_build_object( 'name' ,'pg_rowalesce' ,'abstract' ,'rowalesce() is like coalesce(), but for rows and other composite types.' ,'description' ,'The pg_rowalesce PostgreSQL extensions its defining feature is the rowalesce() function.' ' rowalesce() is like coalesce(), but for rows and other composite types. From its arbitrary' ' number of argument rows, for each field/column, rowalesce() takes the value from the first row' ' for which that particular field/column has a not null value.' ,'version' ,( select pg_extension.extversion from pg_catalog.pg_extension where pg_extension.extname = 'pg_rowalesce' ) ,'maintainer' ,array[ 'Rowan Rodrik van der Molen ' ] ,'license' ,'postgresql' ,'prereqs' ,'{ "runtime": { "requires": { "hstore": 0 } }, "test": { "requires": { "pgtap": 0 } } }'::jsonb ,'provides' ,('{ "pg_rowalesce": { "file": "pg_rowalesce--0.1.10.sql", "version": "' || ( select pg_extension.extversion from pg_catalog.pg_extension where pg_extension.extname = 'pg_rowalesce' ) || '", "docfile": "README.md" } }')::jsonb ,'resources' ,'{ "homepage": "https://blog.bigsmoke.us/tag/pg_rowalesce", "bugtracker": { "web": "https://github.com/bigsmoke/pg_rowalesce/issues" }, "repository": { "url": "https://github.com/bigsmoke/pg_rowalesce.git", "web": "https://github.com/bigsmoke/pg_rowalesce", "type": "git" } }'::jsonb ,'meta-spec' ,'{ "version": "1.0.0", "url": "https://pgxn.org/spec/" }'::jsonb ,'generated_by' ,'`select pg_rowalesce_meta_pgxn()`' ,'tags' ,array[ 'coalesce', 'jsonb', 'plpgsql', 'function', 'functions', 'table' ] ); comment on function pg_rowalesce_meta_pgxn() is $md$Returns the JSON meta data that has to go into the `META.json` file needed for PGXN—PostgreSQL Extension Network—packages. The `Makefile` includes a recipe to allow the developer to: `make META.json` to refresh the meta file with the function's current output, including the `default_version`. `pg_rowalesce` can be found on PGXN: https://pgxn.org/dist/pg_readme/ $md$; -------------------------------------------------------------------------------------------------------------- create function rowalesce(variadic anyarray, out anyelement) immutable leakproof parallel safe set search_path from current language plpgsql as $$ declare _row_json json; _column_name name; begin --$2 := $1[1]; -- NOTE: This is not ideal. Ideally, we would go through the argument list from left to right and ignore -- not-NULL values from previous argument rows. -- Even more ideally, this would be a C function. for _row_json in select json_strip_nulls(row_to_json(row_args.*)) as row_json from unnest($1) with ordinality as row_args order by ordinality desc loop $2 := json_populate_record($2, _row_json); end loop; end; $$; comment on function rowalesce(variadic anyarray) is $md$Coalesce the column/field values in the order of the argument records given. Each argument must be of the same _explicit_ row type. $md$; -------------------------------------------------------------------------------------------------------------- create function rowalesce(in jsonb, variadic anyarray, out anyelement) immutable leakproof parallel safe set search_path from current language plpgsql as $$ begin assert jsonb_typeof($1) = 'object'; $3 := rowalesce(variadic jsonb_populate_record($3, $1) || $2); end; $$; comment on function rowalesce(jsonb, variadic anyarray) is $md$Coalesce the `JSONB` (first) argument with an arbitrary number of explicitly-typed record/row arguments. Example: ```sql select rowalesce( '{"col1": 4, "col4": "2022-01-01"}'::jsonb, null::_tbl, row(null, null, false, null) ); ``` $md$; -------------------------------------------------------------------------------------------------------------- create function rowalesce(in hstore, variadic anyarray, out anyelement) immutable leakproof parallel safe set search_path from current language plpgsql as $$ begin $3 := rowalesce(variadic populate_record($3, $1) || $2); end; $$; comment on function rowalesce(hstore, variadic anyarray) is $md$Coalesces the fields in the `hstore` with the field values from each successive record-type argument. Example: ```sql create type myrow ( col1 int ,col2 text ,col3 timestamptz ); select rowalesce( '"col1"=>"42", "col3"=>"2000-01-01"'::hstore, row(null, 'meaning', null)::myrow, ); ``` You can also use this function to rowalesce with rows of the unspecified type `record`—just wrap it as `hstore(record)`: ```sql create type myrow ( col1 int ,col2 text ,col3 timestamptz ); create function record_rowalesce(in record, variadic anyarray, out anyelement) immutable leakproof parallel safe language plpgsql as $$ begin $3 := rowalesce(hstore($1), variadic $2); end; $$; create function use_record_rowalesce() language plpgsql as $$ declare _untyped_rec record; _typed_row begin select 4::int as col1, now() as col3 into _untyped_rec; _typed_row := record_rowalesce(_untyped_rec, null::myrow); end; $$; ``` $md$; -------------------------------------------------------------------------------------------------------------- create function rowalesce(anyelement, jsonb, out anyelement) returns anyelement immutable leakproof parallel safe set search_path from current language plpgsql as $$ begin $3 := jsonb_populate_record($3, $2 || jsonb_strip_nulls(to_jsonb($1))); end; $$; -------------------------------------------------------------------------------------------------------------- create function table_defaults(pg_class$ regclass, include_columns$ hstore = null) returns hstore set search_path from current language plpgsql as $$ declare _record record; _default_expressions_to_execute text; begin _default_expressions_to_execute := ( select string_agg( format( '%s AS %I', pg_get_expr(pg_attrdef.adbin, pg_attrdef.adrelid), pg_attribute.attname ), ', ' ) from pg_catalog.pg_attribute join pg_catalog.pg_attrdef on pg_attrdef.adrelid = pg_attribute.attrelid and pg_attrdef.adnum = pg_attribute.attnum where pg_attribute.attrelid = $1.oid and pg_attribute.attnum > 0 and pg_attribute.atthasdef = true and pg_attribute.attgenerated = '' -- zero byte and ( include_columns$ is null or coalesce((include_columns$ -> pg_attribute.attname)::bool, false) ) ); if _default_expressions_to_execute is null then return ''::hstore; end if; execute 'SELECT ' || _default_expressions_to_execute into _record; return hstore(_record); end; $$; comment on function table_defaults(regclass, hstore) is $md$Get the (given) column default values for the given table. $md$; -------------------------------------------------------------------------------------------------------------- create function rowalesce_with_defaults(variadic anyarray, out anyelement) set search_path from current language plpgsql as $$ begin $2 := rowalesce(variadic $1); $2 := populate_record( $2, table_defaults( pg_typeof($2)::name::regclass, hstore(( select array_agg(array[col.key, 'true']) from each(hstore($2)) as col where col.value is null )) ) ); end; $$; comment on function rowalesce_with_defaults(variadic anyarray) is $md$Coalesces the column values in the order of the records given and falls back to column defaults. The argument may be `NULL` (coerced to the correct type) if you just want the column defaults for a table type. $md$; -------------------------------------------------------------------------------------------------------------- create function rowalesce_with_defaults(in jsonb, variadic anyarray, out anyelement) volatile set search_path from current language plpgsql as $$ begin assert jsonb_typeof($1) = 'object'; $3 := rowalesce_with_defaults( variadic jsonb_populate_record($3, $1) || $2 ); end; $$; -------------------------------------------------------------------------------------------------------------- create function rowalesce_with_defaults(in hstore, variadic anyarray, out anyelement) volatile set search_path from current language plpgsql as $$ begin $3 := rowalesce_with_defaults( variadic populate_record($3, $1) || $2 ); end; $$; -------------------------------------------------------------------------------------------------------------- create function record_rowalesce_with_defaults(in record, variadic anyarray, out anyelement) volatile set search_path from current language plpgsql as $$ begin $3 := rowalesce_with_defaults(hstore($1), variadic $2); end; $$; comment on function record_rowalesce_with_defaults(record, variadic anyarray) is $md$This function could not be named plain `rowalesce_with-defaults()`, because Postgres considers `rowalesce_with_defaults(record, variadic anyarray)` ambiguous with `rowalesce_with_defaults(variadic anyarray)`. Also, it doesn't add much to calling `rowalesce_with_defaults(hstore, variadic anyarray)` directly and feeding it a `hstore(record)`. Yet, I decided to keep it (for now) for documentation sake. I may still change my mind in a later release (but not any more after 1.0). $md$; -------------------------------------------------------------------------------------------------------------- create function insert_row(inout anyelement) returns anyelement volatile set search_path from current language plpgsql as $$ declare _non_generated_columns name[]; begin _non_generated_columns := array( select pg_attribute.attname from pg_catalog.pg_attribute where pg_attribute.attrelid = pg_typeof($1)::text::regclass::oid and pg_attribute.attnum > 0 and pg_attribute.attidentity = '' -- zero byte and pg_attribute.attgenerated = '' -- zero byte order by pg_attribute.attnum ); execute 'INSERT INTO ' || pg_typeof($1) || ' (' || array_to_string(_non_generated_columns, ', ') || ') VALUES (' || array_to_string(( select array_agg(format('$1.%I', col)) from unnest(_non_generated_columns) as col ), ', ') || ')' || ' RETURNING *' using $1 into $1; -- This overrides the local _copy of_ the input parameter, not the (IN) param itself! end; $$; comment on function insert_row(inout anyelement) is $md$Wraps around `INSERT INTO … RETURNING` so that it''s friendlier to use in some contexts. $md$; -------------------------------------------------------------------------------------------------------------- create procedure test__pg_rowalesce() language plpgsql set search_path from current set plpgsql.check_asserts to true as $$ declare _rec record; begin create local temporary table _tbl ( col1 int default 9 ,col2 text default 'iets' ,col3 bool default true ,col4 timestamptz default now() ); assert table_defaults('_tbl')::text = hstore('"col1"=>"9","col2"=>"iets","col3"=>"t",col4=>"' || now()::text || '"')::text; assert table_defaults('_tbl', ''::hstore) = ''::hstore ,'table_defaults() should be okay with having nothing to do.'; assert rowalesce( row(4, null, null, now())::_tbl ,row(5, 'blah', null, now() + interval '1 day')::_tbl ) = row(4, 'blah', null, now())::_tbl ,'NULL values (and _only_ NULL values) should be rowalesced.'; assert rowalesce_with_defaults( row(4, null, null, now())::_tbl ,row(5, 'blah', null, now() + interval '1 day')::_tbl ) = row(4, 'blah', true, now())::_tbl ,'NULL values in arguments should be rowalesced, and fall back to table defaults.'; assert rowalesce( '{"col1": 4, "col4": "2022-01-01"}'::jsonb, null::_tbl, row(null, null, false, null)::_tbl ) = row(4, null, false, '2022-01-01'::timestamptz)::_tbl; assert rowalesce_with_defaults( '{"col1": 4, "col4": "2022-01-01"}'::jsonb, null::_tbl ) = row(4, 'iets', true, '2022-01-01'::timestamptz)::_tbl; assert rowalesce( '{"col1": 4, "col4": "2022-01-01"}'::jsonb, row(5, 'blah', null, '2022-12-31'::timestamptz)::_tbl, null::_tbl, null::_tbl ) = row(4, 'blah', null, '2022-01-01'::timestamptz)::_tbl; assert rowalesce_with_defaults( '{"col4": "2022-01-01"}'::jsonb, row(null, 'blah', null, '2022-12-31'::timestamptz)::_tbl, null::_tbl, null::_tbl ) = row(9, 'blah', true, '2022-01-01'::timestamptz)::_tbl; assert rowalesce( 'col1=>4,col4=>"2022-01-01"'::hstore, row(5, 'blah', null, '2022-12-31'::timestamptz)::_tbl ) = row(4, 'blah', null, '2022-01-01'::timestamptz)::_tbl; assert rowalesce_with_defaults( 'col1=>4,col4=>"2022-01-01"'::hstore, row(5, 'blah', null, '2022-12-31'::timestamptz)::_tbl ) = row(4, 'blah', true, '2022-01-01'::timestamptz)::_tbl; /* assert record_rowalesce_with_defaults( _rec, row(5, 'blah', null, now() + interval '1 day')::_tbl ) = row(4, 'blah', true, now())::_tbl; */ raise transaction_rollback; -- I could have use any error code, but this one seemed to fit best. exception when transaction_rollback then end; $$; --------------------------------------------------------------------------------------------------------------