diff --git a/contrib/hstore/Makefile b/contrib/hstore/Makefile index 72376d9007..c4e339b57c 100644 --- a/contrib/hstore/Makefile +++ b/contrib/hstore/Makefile @@ -7,10 +7,12 @@ OBJS = \ hstore_gin.o \ hstore_gist.o \ hstore_io.o \ - hstore_op.o + hstore_op.o \ + hstore_subs.o EXTENSION = hstore DATA = hstore--1.4.sql \ + hstore--1.7--1.8.sql \ hstore--1.6--1.7.sql \ hstore--1.5--1.6.sql \ hstore--1.4--1.5.sql \ diff --git a/contrib/hstore/expected/hstore.out b/contrib/hstore/expected/hstore.out index 8901079438..fdcc3920ce 100644 --- a/contrib/hstore/expected/hstore.out +++ b/contrib/hstore/expected/hstore.out @@ -1560,6 +1560,29 @@ select json_agg(q) from (select f1, hstore_to_json_loose(f2) as f2 from test_jso {"f1":"rec2","f2":{"b": false, "c": "null", "d": -12345, "e": "012345.6", "f": -1.234, "g": 0.345e-4, "a key": 2}}] (1 row) +-- Test subscripting +insert into test_json_agg default values; +select f2['d'], f2['x'] is null as x_isnull from test_json_agg; + f2 | x_isnull +--------+---------- + 12345 | t + -12345 | t + | t +(3 rows) + +select f2['d']['e'] from test_json_agg; -- error +ERROR: hstore allows only one subscript +select f2['d':'e'] from test_json_agg; -- error +ERROR: hstore allows only one subscript +update test_json_agg set f2['d'] = f2['e'], f2['x'] = 'xyzzy'; +select f2 from test_json_agg; + f2 +--------------------------------------------------------------------------------------------------------------------- + "b"=>"t", "c"=>NULL, "d"=>"012345", "e"=>"012345", "f"=>"1.234", "g"=>"2.345e+4", "x"=>"xyzzy", "a key"=>"1" + "b"=>"f", "c"=>"null", "d"=>"012345.6", "e"=>"012345.6", "f"=>"-1.234", "g"=>"0.345e-4", "x"=>"xyzzy", "a key"=>"2" + "d"=>NULL, "x"=>"xyzzy" +(3 rows) + -- Check the hstore_hash() and hstore_hash_extended() function explicitly. SELECT v as value, hstore_hash(v)::bit(32) as standard, hstore_hash_extended(v, 0)::bit(32) as extended0, diff --git a/contrib/hstore/hstore--1.7--1.8.sql b/contrib/hstore/hstore--1.7--1.8.sql new file mode 100644 index 0000000000..d80a138465 --- /dev/null +++ b/contrib/hstore/hstore--1.7--1.8.sql @@ -0,0 +1,13 @@ +/* contrib/hstore/hstore--1.7--1.8.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION hstore UPDATE TO '1.8'" to load this file. \quit + +CREATE FUNCTION hstore_subscript_handler(internal) +RETURNS internal +AS 'MODULE_PATHNAME', 'hstore_subscript_handler' +LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE; + +ALTER TYPE hstore SET ( + SUBSCRIPT = hstore_subscript_handler +); diff --git a/contrib/hstore/hstore.control b/contrib/hstore/hstore.control index f0da772429..89e3c746c4 100644 --- a/contrib/hstore/hstore.control +++ b/contrib/hstore/hstore.control @@ -1,6 +1,6 @@ # hstore extension comment = 'data type for storing sets of (key, value) pairs' -default_version = '1.7' +default_version = '1.8' module_pathname = '$libdir/hstore' relocatable = true trusted = true diff --git a/contrib/hstore/hstore_subs.c b/contrib/hstore/hstore_subs.c new file mode 100644 index 0000000000..e52de04f1a --- /dev/null +++ b/contrib/hstore/hstore_subs.c @@ -0,0 +1,297 @@ +/*------------------------------------------------------------------------- + * + * hstore_subs.c + * Subscripting support functions for hstore. + * + * This is a great deal simpler than array_subs.c, because the result of + * subscripting an hstore is just a text string (the value for the key). + * We do not need to support array slicing notation, nor multiple subscripts. + * Less obviously, because the subscript result is never a SQL container + * type, there will never be any nested-assignment scenarios, so we do not + * need a fetch_old function. In turn, that means we can drop the + * check_subscripts function and just let the fetch and assign functions + * do everything. + * + * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * contrib/hstore/hstore_subs.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "executor/execExpr.h" +#include "hstore.h" +#include "nodes/nodeFuncs.h" +#include "nodes/subscripting.h" +#include "parser/parse_coerce.h" +#include "parser/parse_expr.h" +#include "utils/builtins.h" + + +/* + * Finish parse analysis of a SubscriptingRef expression for hstore. + * + * Verify there's just one subscript, coerce it to text, + * and set the result type of the SubscriptingRef node. + */ +static void +hstore_subscript_transform(SubscriptingRef *sbsref, + List *indirection, + ParseState *pstate, + bool isSlice, + bool isAssignment) +{ + A_Indices *ai; + Node *subexpr; + + /* We support only single-subscript, non-slice cases */ + if (isSlice || list_length(indirection) != 1) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("hstore allows only one subscript"), + parser_errposition(pstate, + exprLocation((Node *) indirection)))); + + /* Transform the subscript expression to type text */ + ai = linitial_node(A_Indices, indirection); + Assert(ai->uidx != NULL && ai->lidx == NULL && !ai->is_slice); + + subexpr = transformExpr(pstate, ai->uidx, pstate->p_expr_kind); + /* If it's not text already, try to coerce */ + subexpr = coerce_to_target_type(pstate, + subexpr, exprType(subexpr), + TEXTOID, -1, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + -1); + if (subexpr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("hstore subscript must have type text"), + parser_errposition(pstate, exprLocation(ai->uidx)))); + + /* ... and store the transformed subscript into the SubscriptRef node */ + sbsref->refupperindexpr = list_make1(subexpr); + sbsref->reflowerindexpr = NIL; + + /* Determine the result type of the subscripting operation; always text */ + sbsref->refrestype = TEXTOID; + sbsref->reftypmod = -1; +} + +/* + * Evaluate SubscriptingRef fetch for hstore. + * + * Source container is in step's result variable (it's known not NULL, since + * we set fetch_strict to true), and the subscript expression is in the + * upperindex[] array. + */ +static void +hstore_subscript_fetch(ExprState *state, + ExprEvalStep *op, + ExprContext *econtext) +{ + SubscriptingRefState *sbsrefstate = op->d.sbsref.state; + HStore *hs; + text *key; + HEntry *entries; + int idx; + text *out; + + /* Should not get here if source hstore is null */ + Assert(!(*op->resnull)); + + /* Check for null subscript */ + if (sbsrefstate->upperindexnull[0]) + { + *op->resnull = true; + return; + } + + /* OK, fetch/detoast the hstore and subscript */ + hs = DatumGetHStoreP(*op->resvalue); + key = DatumGetTextPP(sbsrefstate->upperindex[0]); + + /* The rest is basically the same as hstore_fetchval() */ + entries = ARRPTR(hs); + idx = hstoreFindKey(hs, NULL, + VARDATA_ANY(key), VARSIZE_ANY_EXHDR(key)); + + if (idx < 0 || HSTORE_VALISNULL(entries, idx)) + { + *op->resnull = true; + return; + } + + out = cstring_to_text_with_len(HSTORE_VAL(entries, STRPTR(hs), idx), + HSTORE_VALLEN(entries, idx)); + + *op->resvalue = PointerGetDatum(out); +} + +/* + * Evaluate SubscriptingRef assignment for hstore. + * + * Input container (possibly null) is in result area, replacement value is in + * SubscriptingRefState's replacevalue/replacenull. + */ +static void +hstore_subscript_assign(ExprState *state, + ExprEvalStep *op, + ExprContext *econtext) +{ + SubscriptingRefState *sbsrefstate = op->d.sbsref.state; + text *key; + Pairs p; + HStore *out; + + /* Check for null subscript */ + if (sbsrefstate->upperindexnull[0]) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("hstore subscript in assignment must not be null"))); + + /* OK, fetch/detoast the subscript */ + key = DatumGetTextPP(sbsrefstate->upperindex[0]); + + /* Create a Pairs entry for subscript + replacement value */ + p.needfree = false; + p.key = VARDATA_ANY(key); + p.keylen = hstoreCheckKeyLen(VARSIZE_ANY_EXHDR(key)); + + if (sbsrefstate->replacenull) + { + p.vallen = 0; + p.isnull = true; + } + else + { + text *val = DatumGetTextPP(sbsrefstate->replacevalue); + + p.val = VARDATA_ANY(val); + p.vallen = hstoreCheckValLen(VARSIZE_ANY_EXHDR(val)); + p.isnull = false; + } + + if (*op->resnull) + { + /* Just build a one-element hstore (cf. hstore_from_text) */ + out = hstorePairs(&p, 1, p.keylen + p.vallen); + } + else + { + /* + * Otherwise, merge the new key into the hstore. Based on + * hstore_concat. + */ + HStore *hs = DatumGetHStoreP(*op->resvalue); + int s1count = HS_COUNT(hs); + int outcount = 0; + int vsize; + char *ps1, + *bufd, + *pd; + HEntry *es1, + *ed; + int s1idx; + int s2idx; + + /* Allocate result without considering possibility of duplicate */ + vsize = CALCDATASIZE(s1count + 1, VARSIZE(hs) + p.keylen + p.vallen); + out = palloc(vsize); + SET_VARSIZE(out, vsize); + HS_SETCOUNT(out, s1count + 1); + + ps1 = STRPTR(hs); + bufd = pd = STRPTR(out); + es1 = ARRPTR(hs); + ed = ARRPTR(out); + + for (s1idx = s2idx = 0; s1idx < s1count || s2idx < 1; ++outcount) + { + int difference; + + if (s1idx >= s1count) + difference = 1; + else if (s2idx >= 1) + difference = -1; + else + { + int s1keylen = HSTORE_KEYLEN(es1, s1idx); + int s2keylen = p.keylen; + + if (s1keylen == s2keylen) + difference = memcmp(HSTORE_KEY(es1, ps1, s1idx), + p.key, + s1keylen); + else + difference = (s1keylen > s2keylen) ? 1 : -1; + } + + if (difference >= 0) + { + HS_ADDITEM(ed, bufd, pd, p); + ++s2idx; + if (difference == 0) + ++s1idx; + } + else + { + HS_COPYITEM(ed, bufd, pd, + HSTORE_KEY(es1, ps1, s1idx), + HSTORE_KEYLEN(es1, s1idx), + HSTORE_VALLEN(es1, s1idx), + HSTORE_VALISNULL(es1, s1idx)); + ++s1idx; + } + } + + HS_FINALIZE(out, outcount, bufd, pd); + } + + *op->resvalue = PointerGetDatum(out); + *op->resnull = false; +} + +/* + * Set up execution state for an hstore subscript operation. + */ +static void +hstore_exec_setup(const SubscriptingRef *sbsref, + SubscriptingRefState *sbsrefstate, + SubscriptExecSteps *methods) +{ + /* Assert we are dealing with one subscript */ + Assert(sbsrefstate->numlower == 0); + Assert(sbsrefstate->numupper == 1); + /* We can't check upperprovided[0] here, but it must be true */ + + /* Pass back pointers to appropriate step execution functions */ + methods->sbs_check_subscripts = NULL; + methods->sbs_fetch = hstore_subscript_fetch; + methods->sbs_assign = hstore_subscript_assign; + methods->sbs_fetch_old = NULL; +} + +/* + * hstore_subscript_handler + * Subscripting handler for hstore. + */ +PG_FUNCTION_INFO_V1(hstore_subscript_handler); +Datum +hstore_subscript_handler(PG_FUNCTION_ARGS) +{ + static const SubscriptRoutines sbsroutines = { + .transform = hstore_subscript_transform, + .exec_setup = hstore_exec_setup, + .fetch_strict = true, /* fetch returns NULL for NULL inputs */ + .fetch_leakproof = true, /* fetch returns NULL for bad subscript */ + .store_leakproof = false /* ... but assignment throws error */ + }; + + PG_RETURN_POINTER(&sbsroutines); +} diff --git a/contrib/hstore/sql/hstore.sql b/contrib/hstore/sql/hstore.sql index a6c2f3a0ce..8d96e30403 100644 --- a/contrib/hstore/sql/hstore.sql +++ b/contrib/hstore/sql/hstore.sql @@ -364,6 +364,14 @@ insert into test_json_agg values ('rec1','"a key" =>1, b => t, c => null, d=> 12 select json_agg(q) from test_json_agg q; select json_agg(q) from (select f1, hstore_to_json_loose(f2) as f2 from test_json_agg) q; +-- Test subscripting +insert into test_json_agg default values; +select f2['d'], f2['x'] is null as x_isnull from test_json_agg; +select f2['d']['e'] from test_json_agg; -- error +select f2['d':'e'] from test_json_agg; -- error +update test_json_agg set f2['d'] = f2['e'], f2['x'] = 'xyzzy'; +select f2 from test_json_agg; + -- Check the hstore_hash() and hstore_hash_extended() function explicitly. SELECT v as value, hstore_hash(v)::bit(32) as standard, hstore_hash_extended(v, 0)::bit(32) as extended0, diff --git a/doc/src/sgml/hstore.sgml b/doc/src/sgml/hstore.sgml index 14a36ade00..080706280e 100644 --- a/doc/src/sgml/hstore.sgml +++ b/doc/src/sgml/hstore.sgml @@ -713,6 +713,39 @@ b + + + In addition to these operators and functions, values of + the hstore type can be subscripted, allowing them to act + like associative arrays. Only a single subscript of type text + can be specified; it is interpreted as a key and the corresponding + value is fetched or stored. For example, + + +CREATE TABLE mytable (h hstore); +INSERT INTO mytable VALUES ('a=>b, c=>d'); +SELECT h['a'] FROM mytable; + h +--- + b +(1 row) + +UPDATE mytable SET h['c'] = 'new'; +SELECT h FROM mytable; + h +---------------------- + "a"=>"b", "c"=>"new" +(1 row) + + + A subscripted fetch returns NULL if the subscript + is NULL or that key does not exist in + the hstore. (Thus, a subscripted fetch is not greatly + different from the -> operator.) + A subscripted update fails if the subscript is NULL; + otherwise, it replaces the value for that key, adding an entry to + the hstore if the key does not already exist. + @@ -767,7 +800,16 @@ CREATE INDEX hidx ON testhstore USING HASH (h); Add a key, or update an existing key with a new value: +UPDATE tab SET h['c'] = '3'; + + Another way to do the same thing is: + UPDATE tab SET h = h || hstore('c', '3'); + + If multiple keys are to be added or changed in one operation, + the concatenation approach is more efficient than subscripting: + +UPDATE tab SET h = h || hstore(array['q', 'w'], array['11', '12']); diff --git a/doc/src/sgml/ref/create_type.sgml b/doc/src/sgml/ref/create_type.sgml index d909ee0d33..d575f16614 100644 --- a/doc/src/sgml/ref/create_type.sgml +++ b/doc/src/sgml/ref/create_type.sgml @@ -333,9 +333,11 @@ CREATE TYPE name return an internal result, which is a pointer to a struct of methods (functions) that implement subscripting. The detailed API for subscript functions appears - in src/include/nodes/subscripting.h; - it may also be useful to read the array implementation - in src/backend/utils/adt/arraysubs.c. + in src/include/nodes/subscripting.h. + It may also be useful to read the array implementation + in src/backend/utils/adt/arraysubs.c, + or the simpler code + in contrib/hstore/hstore_subs.c. Additional information appears in below.