Hi When I wrote an reply to questing
https://stackoverflow.com/questions/66523737/postgresql-10-pl-pgsql-test-if-column-exits-in-a-record-variable I found an interesting idea to have some basic functions and operators for record type (similar to json, jsonb or hstore). Now we can do almost all tasks on record type by cast to jsonb type. But this transformation has some overhead (and for some tasks is not necessary), and it is not too intuitive too. I don't think so we need full functionality like hstore or jsonb (minimally because record type cannot be persistent and indexed), but some basic functionality can be useful. -- tests of basic helper functions for record type do $$ declare r record; k text; v text; t text; begin select oid, relname, relnamespace, reltype from pg_class limit 1 into r; if not r ? 'xxx' then raise notice 'pg_class has not column xxx'; end if; if r ? 'relname' then raise notice 'pg_class has column relname'; end if; foreach k in array record_keys_array(r) loop raise notice '% => %', k, r->>k; end loop; raise notice '---'; -- second (slower) variant for k in select * from record_keys(r) loop raise notice '% => %', k, r->>k; end loop; raise notice '---'; -- complete unpacking for k, v, t in select * from record_each_text(r) loop raise notice '% => %(%)', k, v, t; end loop; end; $$; What do you think about this proposal? Comments, notes? Regards Pavel
diff --git a/src/backend/utils/adt/rowtypes.c b/src/backend/utils/adt/rowtypes.c index 23787a6ae7..664ccc6c44 100644 --- a/src/backend/utils/adt/rowtypes.c +++ b/src/backend/utils/adt/rowtypes.c @@ -20,9 +20,11 @@ #include "access/htup_details.h" #include "catalog/pg_type.h" #include "common/hashfn.h" +#include "executor/spi.h" #include "funcapi.h" #include "libpq/pqformat.h" #include "miscadmin.h" +#include "utils/array.h" #include "utils/builtins.h" #include "utils/datum.h" #include "utils/lsyscache.h" @@ -67,6 +69,15 @@ typedef struct RecordCompareData ColumnCompareData columns[FLEXIBLE_ARRAY_MEMBER]; } RecordCompareData; +/* + * Structure used for list of keys + */ +typedef struct +{ + ArrayBuildState *astate; + Tuplestorestate *tupstore; + TupleDesc tupdesc; +} RecordKeysOutputData; /* * record_in - input routine for any composite type. @@ -2015,3 +2026,360 @@ hash_record_extended(PG_FUNCTION_ARGS) PG_RETURN_UINT64(result); } + +/* + * Few simple functions and operators for work with record type. + */ + +/* + * Returns true, if record has a field on top level. + */ +Datum +record_exists(PG_FUNCTION_ARGS) +{ + HeapTupleHeader rec = PG_GETARG_HEAPTUPLEHEADER(0); + char *fname = text_to_cstring(PG_GETARG_TEXT_PP(1)); + Oid tupType; + int32 tupTypmod; + TupleDesc tupdesc; + bool result = false; + int i; + + /* Extract type info from the tuple itself */ + tupType = HeapTupleHeaderGetTypeId(rec); + tupTypmod = HeapTupleHeaderGetTypMod(rec); + tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + char *attname; + + if (att->attisdropped) + continue; + + attname = NameStr(att->attname); + + if (strcmp(fname, attname) == 0) + { + result = true; + break; + } + } + + ReleaseTupleDesc(tupdesc); + + PG_RETURN_BOOL(result); +} + +/* + * Returns text value of record field. Raise an error when required + * key doesn't exists + */ +Datum +record_field_text(PG_FUNCTION_ARGS) +{ + HeapTupleHeader rec = PG_GETARG_HEAPTUPLEHEADER(0); + char *fname = text_to_cstring(PG_GETARG_TEXT_P(1)); + HeapTupleData tuple; + Oid tupType; + int32 tupTypmod; + TupleDesc tupdesc; + int fno; + bool isnull = true; + char *outstr = NULL; + + /* Extract type info from the tuple itself */ + tupType = HeapTupleHeaderGetTypeId(rec); + tupTypmod = HeapTupleHeaderGetTypMod(rec); + tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod); + + /* Build a temporary HeapTuple control structure */ + tuple.t_len = HeapTupleHeaderGetDatumLength(rec); + ItemPointerSetInvalid(&(tuple.t_self)); + tuple.t_tableOid = InvalidOid; + tuple.t_data = rec; + + fno = SPI_fnumber(tupdesc, fname); + if (fno != SPI_ERROR_NOATTRIBUTE) + { + Datum value = SPI_getbinval(&tuple, tupdesc, fno, &isnull); + + if (!isnull) + { + bool typIsVarlena; + Oid typoutput; + FmgrInfo proc; + Oid typeid; + + typeid = SPI_gettypeid(tupdesc, fno); + getTypeOutputInfo(typeid, &typoutput, &typIsVarlena); + fmgr_info(typoutput, &proc); + outstr = OutputFunctionCall(&proc, value); + } + } + + ReleaseTupleDesc(tupdesc); + + if (!isnull) + { + Assert(outstr); + + PG_RETURN_TEXT_P(cstring_to_text(outstr)); + } + + PG_RETURN_NULL(); +} + +/* + * Returns set of composite type (key text, value text, type text) of all + * fields in record. + */ +Datum +record_each_text(PG_FUNCTION_ARGS) +{ + HeapTupleHeader rec = PG_GETARG_HEAPTUPLEHEADER(0); + HeapTupleData tuple; + Oid tupType; + int32 tupTypmod; + TupleDesc tupdesc; + int natts; + Datum *values; + bool *nulls; + int i; + + ReturnSetInfo *rsi = (ReturnSetInfo *) fcinfo->resultinfo; + Tuplestorestate *rstupstore; + HeapTuple rstuple; + TupleDesc rstupdesc; + Datum rsvalues[3]; + bool rsnulls[3] = {false, false, false}; + + MemoryContext old_cxt; + + /* Extract type info from the tuple itself */ + tupType = HeapTupleHeaderGetTypeId(rec); + tupTypmod = HeapTupleHeaderGetTypMod(rec); + tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod); + natts = tupdesc->natts; + + /* check to see if caller supports us returning a tuplestore */ + if (rsi == NULL || !IsA(rsi, ReturnSetInfo)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("set-valued function called in context that cannot accept a set"))); + if (!(rsi->allowedModes & SFRM_Materialize)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("materialize mode required, but it is not allowed in this context"))); + + if (!rsi || !IsA(rsi, ReturnSetInfo) || + (rsi->allowedModes & SFRM_Materialize) == 0 || + rsi->expectedDesc == NULL) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("set-valued function called in context that " + "cannot accept a set"))); + + /* OK, prepare tuplestore in per-query memory */ + old_cxt = MemoryContextSwitchTo(rsi->econtext->ecxt_per_query_memory); + + rstupdesc = CreateTupleDescCopy(rsi->expectedDesc); + rstupstore = tuplestore_begin_heap(true, false, work_mem); + + MemoryContextSwitchTo(old_cxt); + + /* Build a temporary HeapTuple control structure */ + tuple.t_len = HeapTupleHeaderGetDatumLength(rec); + ItemPointerSetInvalid(&(tuple.t_self)); + tuple.t_tableOid = InvalidOid; + tuple.t_data = rec; + + values = (Datum *) palloc(natts * sizeof(Datum)); + nulls = (bool *) palloc(natts * sizeof(bool)); + + /* Break down the tuple into fields */ + heap_deform_tuple(&tuple, tupdesc, values, nulls); + + for (i = 0; i < natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + Oid typ = att->atttypid; + int32 typmod = att->atttypmod; + + /* Ignore dropped columns */ + if (att->attisdropped) + continue; + + rsvalues[0] = CStringGetTextDatum(NameStr(att->attname)); + rsvalues[2] = CStringGetTextDatum(format_type_with_typemod(typ, typmod)); + + if (!nulls[i]) + { + char *outstr; + bool typIsVarlena; + Oid typoutput; + FmgrInfo proc; + + getTypeOutputInfo(typ, &typoutput, &typIsVarlena); + fmgr_info_cxt(typoutput, &proc, old_cxt); + outstr = OutputFunctionCall(&proc, values[i]); + + rsvalues[1] = CStringGetTextDatum(outstr); + rsnulls[1] = false; + } + else + rsnulls[1] = true; + + rstuple = heap_form_tuple(rstupdesc, rsvalues, rsnulls); + + tuplestore_puttuple(rstupstore, rstuple); + } + + ReleaseTupleDesc(tupdesc); + + /* clean up and return the tuplestore */ + tuplestore_donestoring(rstupstore); + + rsi->returnMode = SFRM_Materialize; + rsi->setResult = rstupstore; + rsi->setDesc = rstupdesc; + + pfree(values); + pfree(nulls); + + return (Datum) 0; +} + +/* + * Add field name to result (table or array) + */ +static void +record_keys_accum_result(RecordKeysOutputData *rkstate, text *fname_value) +{ + if (rkstate->tupstore) + { + Datum values[1]; + bool nulls[1]; + + values[0] = PointerGetDatum(fname_value); + nulls[0] = false; + + tuplestore_putvalues(rkstate->tupstore, + rkstate->tupdesc, + values, + nulls); + } + else + { + rkstate->astate = accumArrayResult(rkstate->astate, + PointerGetDatum(fname_value), + false, + TEXTOID, + CurrentMemoryContext); + } +} + +/* + * Prepares result of record_keys and record_keys_array functions. + * rkstate should be initialized before. + */ +static void +record_keys_internal(FunctionCallInfo fcinfo, RecordKeysOutputData *rkstate) +{ + HeapTupleHeader rec = PG_GETARG_HEAPTUPLEHEADER(0); + Oid tupType; + int32 tupTypmod; + TupleDesc tupdesc; + int i; + + /* Extract type info from the tuple itself */ + tupType = HeapTupleHeaderGetTypeId(rec); + tupTypmod = HeapTupleHeaderGetTypMod(rec); + tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + char *attname; + + if (att->attisdropped) + continue; + + attname = NameStr(att->attname); + + record_keys_accum_result(rkstate, cstring_to_text(attname)); + } + + ReleaseTupleDesc(tupdesc); +} + +/* + * Returns an array of keys of record on top level + */ +Datum +record_keys(PG_FUNCTION_ARGS) +{ + RecordKeysOutputData rkstate; + ReturnSetInfo *rsi; + MemoryContext old_cxt; + + rsi = (ReturnSetInfo *) fcinfo->resultinfo; + + /* check to see if caller supports us returning a tuplestore */ + if (rsi == NULL || !IsA(rsi, ReturnSetInfo)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("set-valued function called in context that cannot accept a set"))); + if (!(rsi->allowedModes & SFRM_Materialize)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("materialize mode required, but it is not allowed in this context"))); + + if (!rsi || !IsA(rsi, ReturnSetInfo) || + (rsi->allowedModes & SFRM_Materialize) == 0 || + rsi->expectedDesc == NULL) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("set-valued function called in context that " + "cannot accept a set"))); + + /* OK, prepare tuplestore in per-query memory */ + old_cxt = MemoryContextSwitchTo(rsi->econtext->ecxt_per_query_memory); + + rkstate.astate = NULL; + rkstate.tupdesc = CreateTupleDescCopy(rsi->expectedDesc); + rkstate.tupstore = tuplestore_begin_heap(true, false, work_mem); + + MemoryContextSwitchTo(old_cxt); + + record_keys_internal(fcinfo, &rkstate); + + tuplestore_donestoring(rkstate.tupstore); + + rsi->returnMode = SFRM_Materialize; + rsi->setResult = rkstate.tupstore; + rsi->setDesc = rkstate.tupdesc; + + return (Datum) 0; +} + +/* + * Returns an array of keys of record on top level + */ +Datum +record_keys_array(PG_FUNCTION_ARGS) +{ + RecordKeysOutputData rkstate; + + /* For array output, rkstate should start as all zeroes */ + memset(&rkstate, 0, sizeof(rkstate)); + + record_keys_internal(fcinfo, &rkstate); + + if (rkstate.astate == NULL) + PG_RETURN_ARRAYTYPE_P(construct_empty_array(TEXTOID)); + + PG_RETURN_ARRAYTYPE_P(makeArrayResult(rkstate.astate, + CurrentMemoryContext)); +} diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat index 85395a81ee..b74e05eb21 100644 --- a/src/include/catalog/pg_operator.dat +++ b/src/include/catalog/pg_operator.dat @@ -3473,4 +3473,10 @@ oprcode => 'multirange_after_multirange', oprrest => 'multirangesel', oprjoin => 'scalargtjoinsel' }, +{ oid => '9335', descr => 'key exists', + oprname => '?', oprleft => 'record', oprright => 'text', oprresult => 'bool', + oprcode => 'record_exists' }, +{ oid => '9336', descr => 'get record field as text', + oprname => '->>', oprleft => 'record', oprright => 'text', oprresult => 'text', + oprcode => 'record_field_text' }, ] diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 506689d8ac..72f8965587 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -11402,4 +11402,27 @@ proname => 'is_normalized', prorettype => 'bool', proargtypes => 'text text', prosrc => 'unicode_is_normalized' }, +# utility functions for record type +{ oid => '9330', + proname => 'record_exists', prorettype => 'bool', + proargtypes => 'record text', prosrc => 'record_exists' }, + +{ oid => '9331', + proname => 'record_field_text', prorettype => 'text', + proargtypes => 'record text', prosrc => 'record_field_text' }, + +{ oid => '9332', descr => 'key value pairs of a record', + proname => 'record_each_text', prorows => '100', proretset => 't', + prorettype => 'record', proargtypes => 'record', + proallargtypes => '{record,text,text,text}', proargmodes => '{i,o,o,o}', + proargnames => '{from_record,key,value,type}', prosrc => 'record_each_text' }, + +{ oid => '9333', descr => 'returns keys of record on top level', + proname => 'record_keys', prorows => '100', proretset => 't', + prorettype => 'text', proargtypes => 'record', prosrc => 'record_keys' }, + +{ oid => '9334', descr => 'returns an array of keys of record on top level', + proname => 'record_keys_array', prorettype => '_text', + proargtypes => 'record', prosrc => 'record_keys_array' } + ] diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out index 86d0665924..baa7f1ee47 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_record.out +++ b/src/pl/plpgsql/src/expected/plpgsql_record.out @@ -791,3 +791,56 @@ table two_int8s_tab; (42,42) (1 row) +-- tests of basic helper functions for record type +do $$ +declare + r record; + k text; v text; t text; +begin + select oid, relname, relnamespace, reltype from pg_class limit 1 into r; + if not r ? 'xxx' then + raise notice 'pg_class has not column xxx'; + end if; + + if r ? 'relname' then + raise notice 'pg_class has column relname'; + end if; + + foreach k in array record_keys_array(r) + loop + raise notice '% => %', k, r->>k; + end loop; + + raise notice '---'; + + -- second (slower) variant + for k in select * from record_keys(r) + loop + raise notice '% => %', k, r->>k; + end loop; + + raise notice '---'; + + -- complete unpacking + for k, v, t in select * from record_each_text(r) + loop + raise notice '% => %(%)', k, v, t; + end loop; +end; +$$; +NOTICE: pg_class has not column xxx +NOTICE: pg_class has column relname +NOTICE: oid => 16385 +NOTICE: relname => complex +NOTICE: relnamespace => 2200 +NOTICE: reltype => 16387 +NOTICE: --- +NOTICE: oid => 16385 +NOTICE: relname => complex +NOTICE: relnamespace => 2200 +NOTICE: reltype => 16387 +NOTICE: --- +NOTICE: oid => 16385(oid) +NOTICE: relname => complex(name) +NOTICE: relnamespace => 2200(oid) +NOTICE: reltype => 16387(oid) diff --git a/src/pl/plpgsql/src/sql/plpgsql_record.sql b/src/pl/plpgsql/src/sql/plpgsql_record.sql index 722048c730..634558ae76 100644 --- a/src/pl/plpgsql/src/sql/plpgsql_record.sql +++ b/src/pl/plpgsql/src/sql/plpgsql_record.sql @@ -518,3 +518,42 @@ insert into two_int8s_tab values (compresult(42)); -- reconnect so we lose any local knowledge of anonymous record types \c - table two_int8s_tab; + +-- tests of basic helper functions for record type +do $$ +declare + r record; + k text; v text; t text; +begin + select oid, relname, relnamespace, reltype from pg_class limit 1 into r; + if not r ? 'xxx' then + raise notice 'pg_class has not column xxx'; + end if; + + if r ? 'relname' then + raise notice 'pg_class has column relname'; + end if; + + foreach k in array record_keys_array(r) + loop + raise notice '% => %', k, r->>k; + end loop; + + raise notice '---'; + + -- second (slower) variant + for k in select * from record_keys(r) + loop + raise notice '% => %', k, r->>k; + end loop; + + raise notice '---'; + + -- complete unpacking + for k, v, t in select * from record_each_text(r) + loop + raise notice '% => %(%)', k, v, t; + end loop; +end; +$$; +