pg.ddx.io  pgsql-hackers@postgresql.org mailing list archive  
help / color / mirror / Atom feed
remaining sql/json patches
300+ messages / 22 participants
[nested] [flat]

Commits from this thread:
  2026-07-09 86ab7f4c72  JSON_TABLE PLAN clause
  2024-04-08 bb766cde63  JSON_TABLE: Add support for NESTED paths and columns
  2024-04-04 de3600452b  Add basic JSON_TABLE() functionality
  2024-03-21 6185c9737c  Add SQL/JSON query functions
  2024-01-24 1edb3b491b  Adjust populate_record_field() to handle errors softly
  2024-01-24 faa2b953ba  Refactor code used by jsonpath executor to fetch variables
  2023-10-02 7fbc75b26e aaaf9449ec  Add soft error handling to some expression nodes
  2023-07-28 e055b6be7e  doc: add missing <returnvalue> and whitespace
  2023-07-26 03734a7fed  Add more SQL/JSON constructor functions
  2023-07-26 254ac5a7c3  Rename a nonterminal used in SQL/JSON grammar
  2023-07-26 b22391a2ff  Some refactoring to export json(b) conversion functions
  2023-07-21 7c7412cae3  Code review for commit b6e1157e7d
  2023-07-20 3c152a27b0  Unify JSON categorize type API and export for external use
  2023-07-13 b6e1157e7d 66a9003e2e  Don't include CaseTestExpr in JsonValueExpr.formatted_expr
  2023-07-13 785480c953 7825a1b01e  Pass constructName to transformJsonValueExpr()
  2023-07-08 01f1f789df 5edf438eeb  Make some indentation in gram.y consistent
* remaining sql/json patches
@ 2023-06-19 08:31  Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-06-19 08:31 UTC (permalink / raw)
  To: pgsql-hackers; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>

Hello.

I'm starting a new thread for $subject per Alvaro's suggestion at [1].

So the following sql/json things still remain to be done:

* sql/json query functions:
    json_exists()
    json_query()
    json_value()

* other sql/json functions:
    json()
    json_scalar()
    json_serialize()

* finally:
    json_table

Attached is the rebased patch for the 1st part.

It also addresses Alvaro's review comments on Apr 4, though see my
comments below.

On Tue, Apr 4, 2023 at 9:36 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
> On 2023-Apr-04, Amit Langote wrote:
> > On Tue, Apr 4, 2023 at 2:16 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
> > > - the gram.y solution to the "ON ERROR/ON EMPTY" clauses is quite ugly.
> > >   I think we could make that stuff use something similar to
> > >   ConstraintAttributeSpec with an accompanying post-processing function.
> > >   That would reduce the number of ad-hoc hacks, which seem excessive.
>>
> > Do you mean the solution involving the JsonBehavior node?
>
> Right.  It has spilled as the separate on_behavior struct in the core
> parser %union in addition to the raw jsbehavior, which is something
> we've gone 30 years without having, and I don't see why we should start
> now.

I looked into trying to make this look like ConstraintAttributeSpec
but came to the conclusion that that's not quite doable in this case.
A "behavior" cannot be represented simply as an integer flag, because
there's `DEFAULT a_expr` to fit in, so it's got to be this
JsonBehavior node.  However...

> This stuff is terrible:
>
> json_exists_error_clause_opt:
>             json_exists_error_behavior ON ERROR_P       { $$ = $1; }
>             | /* EMPTY */                               { $$ = NULL; }
>         ;
>
> json_exists_error_behavior:
>             ERROR_P     { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); }
>             | TRUE_P        { $$ = makeJsonBehavior(JSON_BEHAVIOR_TRUE, NULL); }
>             | FALSE_P       { $$ = makeJsonBehavior(JSON_BEHAVIOR_FALSE, NULL); }
>             | UNKNOWN       { $$ = makeJsonBehavior(JSON_BEHAVIOR_UNKNOWN, NULL); }
>         ;
>
> json_value_behavior:
>             NULL_P      { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); }
>             | ERROR_P       { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); }
>             | DEFAULT a_expr    { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); }
>         ;
>
> json_value_on_behavior_clause_opt:
>             json_value_behavior ON EMPTY_P
>                                     { $$.on_empty = $1; $$.on_error = NULL; }
>             | json_value_behavior ON EMPTY_P json_value_behavior ON ERROR_P
>                                     { $$.on_empty = $1; $$.on_error = $4; }
>             | json_value_behavior ON ERROR_P
>                                     { $$.on_empty = NULL; $$.on_error = $1; }
>             |  /* EMPTY */
>                                     { $$.on_empty = NULL; $$.on_error = NULL; }
>         ;
>
> json_query_behavior:
>             ERROR_P     { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); }
>             | NULL_P        { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); }
>             | EMPTY_P ARRAY { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); }
>             /* non-standard, for Oracle compatibility only */
>             | EMPTY_P       { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); }
>             | EMPTY_P OBJECT_P  { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); }
>             | DEFAULT a_expr    { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); }
>         ;
>
> json_query_on_behavior_clause_opt:
>             json_query_behavior ON EMPTY_P
>                                     { $$.on_empty = $1; $$.on_error = NULL; }
>             | json_query_behavior ON EMPTY_P json_query_behavior ON ERROR_P
>                                     { $$.on_empty = $1; $$.on_error = $4; }
>             | json_query_behavior ON ERROR_P
>                                     { $$.on_empty = NULL; $$.on_error = $1; }
>             |  /* EMPTY */
>                                     { $$.on_empty = NULL; $$.on_error = NULL; }
>         ;
>
> Surely this can be made cleaner.

...I've managed to reduce the above down to:

    MergeWhenClause *mergewhen;
    struct KeyActions *keyactions;
    struct KeyAction *keyaction;
+   JsonBehavior *jsbehavior;
...
+%type <jsbehavior> json_value_behavior
+                   json_query_behavior
+                   json_exists_behavior
...
+json_query_behavior:
+           ERROR_P     { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); }
+           | NULL_P        { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); }
+           | DEFAULT a_expr    { $$ =
makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); }
+           | EMPTY_P ARRAY { $$ =
makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); }
+           | EMPTY_P OBJECT_P  { $$ =
makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); }
+           /* non-standard, for Oracle compatibility only */
+           | EMPTY_P       { $$ =
makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); }
+       ;
+
+json_exists_behavior:
+           ERROR_P     { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); }
+           | TRUE_P        { $$ = makeJsonBehavior(JSON_BEHAVIOR_TRUE, NULL); }
+           | FALSE_P       { $$ =
makeJsonBehavior(JSON_BEHAVIOR_FALSE, NULL); }
+           | UNKNOWN       { $$ =
makeJsonBehavior(JSON_BEHAVIOR_UNKNOWN, NULL); }
+       ;
+
+json_value_behavior:
+           NULL_P      { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); }
+           | ERROR_P       { $$ =
makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); }
+           | DEFAULT a_expr    { $$ =
makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); }
+       ;

Though, that does mean that there are now more rules for
func_expr_common_subexpr to implement the variations of ON ERROR, ON
EMPTY clauses for each of JSON_EXISTS, JSON_QUERY, and JSON_VALUE.

> By the way -- that comment about clauses being non-standard, can you
> spot exactly *which* clauses that comment applies to?

I've moved comment as shown above to make clear that a bare EMPTY_P is
needed for Oracle compatibility

On Tue, Apr 4, 2023 at 2:16 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
> - the changes in formatting.h have no explanation whatsoever.  At the
>   very least, the new function should have a comment in the .c file.
>   (And why is it at end of file?  I bet there's a better location)

Apparently, the newly exported routine is needed in the JSON-specific
subroutine for the planner's contain_mutable_functions_walker(), to
check if a JsonExpr's path_spec contains any timezone-dependent
constant.  In the attached, I've changed the newly exported function's
name as follows:

datetime_format_flags -> datetime_format_has_tz

which let me do away with exporting those DCH_* constants in formatting.h.

> - some nasty hacks are being used in the ECPG grammar with no tests at
>   all.  It's easy to add a few lines to the .pgc file I added in prior
>   commits.

Ah, those ecpg.trailer changes weren't in the original commit that
added added SQL/JSON query functions (1a36bc9dba8ea), but came in
5f0adec2537d, 83f1c7b742e8 to fix some damage caused by the former's
making STRING a keyword.  If I don't include the ecpg.trailer bit,
test_informix.pgc fails, so I think the change is already covered.

> - Some functions in jsonfuncs.c have changed from throwing hard errors
>   into soft ones.  I think this deserves more commentary.

I've merged the delta patch I had posted earlier addressing this [2]
into the attached.

> - func.sgml: The new functions are documented in a separate table for no
>   reason that I can see.  Needs to be merged into one of the existing
>   tables.  I didn't actually review the docs.

Hmm, so we already have "SQL/JSON Testing Functions" that were
committed into v16 in a separate table (Table 9.48) under "9.16.1.
Processing and Creating JSON Data".  So, I don't see a problem with
adding "SQL/JSON Query Functions" in a separate table, though maybe it
should not be under the same sub-section.  Maybe under "9.16.2. The
SQL/JSON Path Language" is more appropriate?

I'll rebase and post the patches for "other sql/json functions" and
"json_table" shortly.

--
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

[1] https://www.postgresql.org/message-id/20230503181732.26hx5ihbdkmzhlyw%40alvherre.pgsql
[2] https://www.postgresql.org/message-id/CA%2BHiwqHGghuFpxE%3DpfUFPT%2BZzKvHWSN4BcrWr%3DZRjd4i4qubfQ%40...

Attachments:

v1-0001-SQL-JSON-query-functions.patch (application/octet-stream, 206.5K) download From 7a599e6cb422744cf342f18b3681440e1de7716b Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Fri, 16 Jun 2023 17:49:18 +0900 Subject: [PATCH v1] SQL/JSON query functions This introduces the SQL/JSON functions for querying JSON data using jsonpath expressions. The functions are: JSON_EXISTS() JSON_QUERY() JSON_VALUE() All of these functions only operate on jsonb. The workaround for now is to cast the argument to jsonb. JSON_EXISTS() tests if the jsonpath expression applied to the jsonb value yields any values. JSON_VALUE() must return a single value, and an error occurs if it tries to return multiple values. JSON_QUERY() must return a json object or array, and there are various WRAPPER options for handling scalar or multi-value results. Both these functions have options for handling EMPTY and ERROR conditions. Nikita Glukhov Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby. Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org --- doc/src/sgml/func.sgml | 147 +++ src/backend/executor/execExpr.c | 432 ++++++++ src/backend/executor/execExprInterp.c | 502 ++++++++- src/backend/jit/llvm/llvmjit_expr.c | 250 +++++ src/backend/jit/llvm/llvmjit_types.c | 5 + src/backend/nodes/makefuncs.c | 15 + src/backend/nodes/nodeFuncs.c | 183 ++++ src/backend/optimizer/path/costsize.c | 3 +- src/backend/optimizer/util/clauses.c | 19 + src/backend/parser/gram.y | 344 ++++++- src/backend/parser/parse_collate.c | 7 + src/backend/parser/parse_expr.c | 493 ++++++++- src/backend/parser/parse_target.c | 15 + src/backend/utils/adt/formatting.c | 44 + src/backend/utils/adt/jsonb.c | 62 ++ src/backend/utils/adt/jsonfuncs.c | 170 +++- src/backend/utils/adt/jsonpath.c | 255 +++++ src/backend/utils/adt/jsonpath_exec.c | 391 ++++++- src/backend/utils/adt/ruleutils.c | 136 +++ src/include/executor/execExpr.h | 172 ++++ src/include/executor/executor.h | 1 + src/include/nodes/execnodes.h | 3 + src/include/nodes/makefuncs.h | 1 + src/include/nodes/parsenodes.h | 59 ++ src/include/nodes/primnodes.h | 109 ++ src/include/parser/kwlist.h | 11 + src/include/utils/formatting.h | 2 +- src/include/utils/jsonb.h | 3 + src/include/utils/jsonfuncs.h | 6 + src/include/utils/jsonpath.h | 27 + src/interfaces/ecpg/preproc/ecpg.trailer | 28 + src/test/regress/expected/json_sqljson.out | 18 + src/test/regress/expected/jsonb_sqljson.out | 1020 +++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/json_sqljson.sql | 11 + src/test/regress/sql/jsonb_sqljson.sql | 318 ++++++ src/tools/pgindent/typedefs.list | 14 + 37 files changed, 5189 insertions(+), 89 deletions(-) create mode 100644 src/test/regress/expected/json_sqljson.out create mode 100644 src/test/regress/expected/jsonb_sqljson.out create mode 100644 src/test/regress/sql/json_sqljson.sql create mode 100644 src/test/regress/sql/jsonb_sqljson.sql diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 5a47ce4343..72c4c86cc7 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16924,6 +16924,153 @@ array w/o UK? | t </tbody> </tgroup> </table> + + <para> + <xref linkend="functions-sqljson-querying"/> details the SQL/JSON + functions that can be used to query JSON data. + </para> + + <note> + <para> + SQL/JSON paths can only be applied to the <type>jsonb</type> type, so it + might be necessary to cast the <replaceable>context_item</replaceable> + argument of these functions to <type>jsonb</type>. + </para> + </note> + + <table id="functions-sqljson-querying"> + <title>SQL/JSON Query Functions</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="func_table_entry"><para role="func_signature"> + Function signature + </para> + <para> + Description + </para> + <para> + Example(s) + </para></entry> + </row> + </thead> + <tbody> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_exists</primary></indexterm> + <function>json_exists</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>TRUE</literal> | <literal>FALSE</literal> |<literal> UNKNOWN</literal> | <literal>ERROR</literal> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns true if the SQL/JSON <replaceable>path_expression</replaceable> + applied to the <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s yields any items. + The <literal>ON ERROR</literal> clause specifies what is returned if + an error occurs. Note that if the <replaceable>path_expression</replaceable> + is <literal>strict</literal>, an error is generated if it yields no items. + The default value is <literal>UNKNOWN</literal> which causes a NULL + result. + </para> + <para> + <literal>json_exists(jsonb '{"key1": [1,2,3]}', 'strict $.key1[*] ? (@ > 2)')</literal> + <returnvalue>t</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'lax $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>f</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'strict $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>ERROR: jsonpath array subscript is out of bounds</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_value</primary></indexterm> + <function>json_value</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> + <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. The extracted value must be + a single <acronym>SQL/JSON</acronym> scalar item. For results that + are objects or arrays, use the <function>json_query</function> + function instead. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + The <literal>ON EMPTY</literal> clause specifies the behavior if the + <replaceable>path_expression</replaceable> yields no value at all. + The <literal>ON ERROR</literal> clause specifies the behavior if an + error occurs as a result of <type>jsonpath</type> evaluation + (including cast to the output type) or execution of + <literal>ON EMPTY</literal> behavior (that was caused by empty result + of <type>jsonpath</type> evaluation). + </para> + <para> + <literal>json_value(jsonb '"123.45"', '$' RETURNING float)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_value(jsonb '"03:04 2015-02-01"', '$.datetime("HH24:MI&nbsp;YYYY-MM-DD")' RETURNING date)</literal> + <returnvalue>2015-02-01</returnvalue> + </para> + <para> + <literal>json_value(jsonb '[1,2]', 'strict $[*]' DEFAULT 9 ON ERROR)</literal> + <returnvalue>9</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_query</primary></indexterm> + <function>json_query</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional> + <optional> { <literal>WITHOUT</literal> | <literal>WITH</literal> { <literal>CONDITIONAL</literal> | <optional><literal>UNCONDITIONAL</literal></optional> } } <optional> <literal>ARRAY</literal> </optional> <literal>WRAPPER</literal> </optional> + <optional> { <literal>KEEP</literal> | <literal>OMIT</literal> } <literal>QUOTES</literal> <optional> <literal>ON SCALAR STRING</literal> </optional> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. + This function must return a JSON string, so if the path expression + returns multiple SQL/JSON items, you must wrap the result using the + <literal>WITH WRAPPER</literal> clause. If the wrapper is + <literal>UNCONDITIONAL</literal>, an array wrapper will always + be applied, even if the returned value is already a single JSON object + or array, but if it is <literal>CONDITIONAL</literal>, it will not be + applied to a single array or object. <literal>UNCONDITIONAL</literal> + is the default. + If the result is a scalar string, by default the value returned will have + surrounding quotes making it a valid JSON value. However, this behavior + is reversed if <literal>OMIT QUOTES</literal> is specified. + The <literal>ON ERROR</literal> and <literal>ON EMPTY</literal> + clauses have similar semantics to those clauses for + <function>json_value</function>. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + </para> + <para> + <literal>json_query(jsonb '[1,[2,3],null]', 'lax $[*][1]' WITH CONDITIONAL WRAPPER)</literal> + <returnvalue>[3]</returnvalue> + </para></entry> + </row> + </tbody> + </tgroup> + </table> + </sect2> <sect2 id="functions-sqljson-path"> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index e6e616865c..0c0dc3a48a 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -48,6 +48,7 @@ #include "utils/array.h" #include "utils/builtins.h" #include "utils/datum.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -87,6 +88,16 @@ static void ExecBuildAggTransCall(ExprState *state, AggState *aggstate, FunctionCallInfo fcinfo, AggStatePerTrans pertrans, int transno, int setno, int setoff, bool ishash, bool nullcheck); +static void ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch); +static Datum GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null); +static JsonCoercionState *ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull); +static JsonItemCoercionsState *ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull); /* @@ -2394,6 +2405,14 @@ ExecInitExprRec(Expr *node, ExprState *state, break; } + case T_JsonExpr: + { + JsonExpr *jexpr = castNode(JsonExpr, node); + + ExecInitJsonExpr(jexpr, state, resv, resnull, &scratch); + break; + } + case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -4161,3 +4180,416 @@ ExecBuildParamSetEqual(TupleDesc desc, return state; } + +/* + * Push steps to evaluate a JsonExpr and its various subsidiary expressions. + */ +static void +ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch) +{ + JsonExprState *jsestate = palloc0(sizeof(JsonExprState)); + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + ListCell *argexprlc; + ListCell *argnamelc; + int skip_step_off = -1; + int passing_args_step_off = -1; + int coercion_step_off = -1; + int coercion_finish_step_off = -1; + int behavior_step_off = -1; + int onempty_expr_step_off = -1; + int onempty_jump_step_off = -1; + int onerror_expr_step_off = -1; + int onerror_jump_step_off = -1; + List *adjust_jumps = NIL; + ListCell *lc; + ExprEvalStep *as; + + jsestate->jsexpr = jexpr; + + /* + * Add steps to compute formatted_expr, pathspec, and PASSING arg + * expressions as things that must be evaluated *before* the actual JSON + * path expression. + */ + ExecInitExprRec((Expr *) jexpr->formatted_expr, state, + &pre_eval->formatted_expr.value, + &pre_eval->formatted_expr.isnull); + ExecInitExprRec((Expr *) jexpr->path_spec, state, + &pre_eval->pathspec.value, + &pre_eval->pathspec.isnull); + + /* + * Before pushing steps for PASSING args, push a step to decide whether to + * skip evaluating the args and the JSON path expression depending on + * whether either of formatted_expr and pathspec is NULL; see + * ExecEvalJsonExprSkip(). + */ + scratch->opcode = EEOP_JSONEXPR_SKIP; + scratch->d.jsonexpr_skip.jsestate = jsestate; + skip_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* PASSING args. */ + jsestate->pre_eval.args = NIL; + passing_args_step_off = state->steps_len; + forboth(argexprlc, jexpr->passing_values, + argnamelc, jexpr->passing_names) + { + Expr *argexpr = (Expr *) lfirst(argexprlc); + String *argname = lfirst_node(String, argnamelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(argname->sval); + var->typid = exprType((Node *) argexpr); + var->typmod = exprTypmod((Node *) argexpr); + + ExecInitExprRec((Expr *) argexpr, state, &var->value, &var->isnull); + + pre_eval->args = lappend(pre_eval->args, var); + } + + /* + * Step for the actual JSON path evaluation; see ExecEvalJson() and + * ExecEvalJsonExpr(). + */ + scratch->opcode = EEOP_JSONEXPR_PATH; + scratch->d.jsonexpr.jsestate = jsestate; + ExprEvalPushStep(state, scratch); + + /* + * Push steps to control the evaluation of expressions based on the result + * of JSON path evaluation. + */ + + /* + * Step to handle ON ERROR and ON EMPTY behavior; see + * ExecEvalJsonExprBehavior(). + */ + scratch->opcode = EEOP_JSONEXPR_BEHAVIOR; + scratch->d.jsonexpr_behavior.jsestate = jsestate; + behavior_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Step(s) to evaluate ON EMPTY expression */ + onempty_expr_step_off = state->steps_len; + if (jexpr->on_empty && + jexpr->on_empty->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_empty->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_empty->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_empty, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onempty_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + } + } + + /* Step(s) to evaluate ON ERROR expression */ + onerror_expr_step_off = state->steps_len; + if (jexpr->on_error && + jexpr->on_error->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_error->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_error->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_error, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onerror_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set later */ + ExprEvalPushStep(state, scratch); + } + } + + /* + * Step to handle applying coercion to the JSON item returned by + * EEOP_JSONEXPR_PATH or to the ON EMPTY/ERROR expression as + * EEOP_JSONEXPR_BEHAVIOR decides; see ExecEvalJsonExprCoercion(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Initialize coercion expression(s). */ + if (jexpr->result_coercion) + { + jsestate->result_jcstate = + ExecInitJsonCoercion(scratch, state, jexpr->result_coercion, + resv, resnull); + /* See the comment above. */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* computed later */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + if (jexpr->coercions) + { + /* + * ExecPrepareJsonItemCoercion() called by ExecEvalJsonExpr() chooses + * one for a given JSON item returned by JsonPathValue(). + */ + jsestate->item_jcstates = + ExecInitJsonItemCoercions(scratch, state, jexpr->coercions, + resv, resnull); + } + + /* + * And a step to clean up after the coercion step; see + * ExecEvalJsonExprCoercionFinish(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION_FINISH; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_finish_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* + * Adjust jump target addresses in various post-eval steps now that we + * have all the steps in place. + */ + + /* EEOP_JSONEXPR_SKIP */ + Assert(skip_step_off >= 0); + as = &state->steps[skip_step_off]; + as->d.jsonexpr_skip.jump_coercion = coercion_step_off; + as->d.jsonexpr_skip.jump_passing_args = passing_args_step_off; + + /* EEOP_JSONEXPR_BEHAVIOR */ + Assert(behavior_step_off >= 0); + as = &state->steps[behavior_step_off]; + as->d.jsonexpr_behavior.jump_onerror_expr = onerror_expr_step_off; + as->d.jsonexpr_behavior.jump_onempty_expr = onempty_expr_step_off; + as->d.jsonexpr_behavior.jump_coercion = coercion_step_off; + as->d.jsonexpr_behavior.jump_skip_coercion = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION */ + Assert(coercion_step_off >= 0); + as = &state->steps[coercion_step_off]; + as->d.jsonexpr_coercion.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION_FINISH */ + Assert(coercion_finish_step_off >= 0); + as = &state->steps[coercion_finish_step_off]; + as->d.jsonexpr_coercion_finish.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion_finish.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JUMP steps */ + + /* + * Ones after ON EMPTY and ON ERROR non-default expressions should jump to + * the coercion step. + */ + if (onempty_jump_step_off >= 0) + { + as = &state->steps[onempty_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + if (onerror_jump_step_off >= 0) + { + as = &state->steps[onerror_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + + /* The rest should jump to the end. */ + foreach(lc, adjust_jumps) + { + as = &state->steps[lfirst_int(lc)]; + as->d.jump.jumpdone = state->steps_len; + } + + /* + * Set RETURNING type's input function used by ExecEvalJsonExprCoercion(). + */ + if (jexpr->omit_quotes || + (jexpr->result_coercion && jexpr->result_coercion->via_io)) + { + Oid typinput; + FmgrInfo *finfo; + + /* lookup the result type's input function */ + getTypeInputInfo(jexpr->returning->typid, &typinput, + &jsestate->input.typioparam); + finfo = palloc0(sizeof(FmgrInfo)); + fmgr_info(typinput, finfo); + jsestate->input.finfo = finfo; + } +} + +/* + * Evaluate a JSON error/empty behavior result. + */ +static Datum +GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null) +{ + *is_null = false; + + switch (behavior->btype) + { + case JSON_BEHAVIOR_EMPTY_ARRAY: + return JsonbPGetDatum(JsonbMakeEmptyArray()); + + case JSON_BEHAVIOR_EMPTY_OBJECT: + return JsonbPGetDatum(JsonbMakeEmptyObject()); + + case JSON_BEHAVIOR_TRUE: + return BoolGetDatum(true); + + case JSON_BEHAVIOR_FALSE: + return BoolGetDatum(false); + + case JSON_BEHAVIOR_NULL: + case JSON_BEHAVIOR_UNKNOWN: + case JSON_BEHAVIOR_EMPTY: + *is_null = true; + return (Datum) 0; + + case JSON_BEHAVIOR_DEFAULT: + /* Always handled in the caller. */ + Assert(false); + return (Datum) 0; + + default: + elog(ERROR, "unrecognized SQL/JSON behavior %d", behavior->btype); + return (Datum) 0; + } +} + +/* Initialize one JsonCoercion for execution. */ +static JsonCoercionState * +ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull) +{ + JsonCoercionState *jcstate = palloc0(sizeof(JsonCoercionState)); + + jcstate->coercion = coercion; + if (coercion && coercion->expr) + { + Datum *save_innermost_caseval; + bool *save_innermost_casenull; + + jcstate->jump_eval_expr = state->steps_len; + + /* Push step(s) to compute cstate->coercion. */ + save_innermost_caseval = state->innermost_caseval; + save_innermost_casenull = state->innermost_casenull; + + state->innermost_caseval = resv; + state->innermost_casenull = resnull; + + ExecInitExprRec((Expr *) coercion->expr, state, resv, resnull); + + state->innermost_caseval = save_innermost_caseval; + state->innermost_casenull = save_innermost_casenull; + } + else + jcstate->jump_eval_expr = -1; + + return jcstate; +} + +/* + * Push steps to evaluate coercions from a given JsonItemCoercions, which + * contains all possible coercions that may need to be applied to JSON + * items coming from evaluating the parent JsonExpr. + */ +static JsonItemCoercionsState * +ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull) +{ + JsonCoercion **coercion; + JsonItemCoercionsState *item_jcstates = palloc0(sizeof(JsonItemCoercionsState)); + JsonCoercionState **item_jcstate; + ExprEvalStep *as; + List *adjust_jumps = NIL; + ListCell *lc; + + /* Push the steps of individual coercions. */ + for (coercion = &coercions->null, + item_jcstate = &item_jcstates->null; + coercion <= &coercions->composite; + coercion++, item_jcstate++) + { + *item_jcstate = ExecInitJsonCoercion(scratch, state, *coercion, + resv, resnull); + + /* Emit JUMP step to skip past other coercions' steps. */ + scratch->opcode = EEOP_JUMP; + + /* + * Remember JUMP step address to set the actual jump target address + * below. + */ + adjust_jumps = lappend_int(adjust_jumps, state->steps_len); + ExprEvalPushStep(state, scratch); + } + + foreach(lc, adjust_jumps) + { + int jump_step_id = lfirst_int(lc); + + as = &state->steps[jump_step_id]; + as->d.jump.jumpdone = state->steps_len; + } + + return item_jcstates; +} diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 7a4d7a4eee..aea390d0ed 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -74,6 +74,7 @@ #include "utils/json.h" #include "utils/jsonb.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/timestamp.h" @@ -152,6 +153,9 @@ static TupleDesc get_cached_rowtype(Oid type_id, int32 typmod, bool *changed); static void ExecEvalRowNullInt(ExprState *state, ExprEvalStep *op, ExprContext *econtext, bool checkisnull); +static Datum ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate); /* fast-path evaluation functions */ static Datum ExecJustInnerVar(ExprState *state, ExprContext *econtext, bool *isnull); @@ -480,6 +484,11 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_XMLEXPR, &&CASE_EEOP_JSON_CONSTRUCTOR, &&CASE_EEOP_IS_JSON, + &&CASE_EEOP_JSONEXPR_SKIP, + &&CASE_EEOP_JSONEXPR_PATH, + &&CASE_EEOP_JSONEXPR_BEHAVIOR, + &&CASE_EEOP_JSONEXPR_COERCION, + &&CASE_EEOP_JSONEXPR_COERCION_FINISH, &&CASE_EEOP_AGGREF, &&CASE_EEOP_GROUPING_FUNC, &&CASE_EEOP_WINDOW_FUNC, @@ -1186,8 +1195,14 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) /* second and third arguments are already set up */ fcinfo_in->isnull = false; + + /* Pass down soft-error capture node if any. */ + fcinfo_in->context = state->escontext; + d = FunctionCallInvoke(fcinfo_in); *op->resvalue = d; + if (SOFT_ERROR_OCCURRED(state->escontext)) + *op->resnull = true; /* Should get null result if and only if str is NULL */ if (str == NULL) @@ -1195,7 +1210,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) Assert(*op->resnull); Assert(fcinfo_in->isnull); } - else + else if (!SOFT_ERROR_OCCURRED(state->escontext)) { Assert(!*op->resnull); Assert(!fcinfo_in->isnull); @@ -1543,6 +1558,38 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_JSONEXPR_PATH) + { + /* too complex for an inline implementation */ + ExecEvalJsonExpr(state, op, econtext); + EEO_NEXT(); + } + + EEO_CASE(EEOP_JSONEXPR_SKIP) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprSkip(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_BEHAVIOR) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprBehavior(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercion(state, op, econtext, + *op->resvalue, *op->resnull)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION_FINISH) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercionFinish(state, op)); + } + EEO_CASE(EEOP_AGGREF) { /* @@ -3740,7 +3787,7 @@ ExecEvalConstraintCheck(ExprState *state, ExprEvalStep *op) { if (!*op->d.domaincheck.checknull && !DatumGetBool(*op->d.domaincheck.checkvalue)) - ereport(ERROR, + errsave(state->escontext, (errcode(ERRCODE_CHECK_VIOLATION), errmsg("value for domain %s violates check constraint \"%s\"", format_type_be(op->d.domaincheck.resulttype), @@ -4092,6 +4139,457 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op) *op->resvalue = BoolGetDatum(res); } +/* + * Evaluate given JsonExpr by performing the specified JSON operation. + * + * This also populates the JsonExprPostEvalState with the information needed + * by the subsequent steps that handle the specified JsonBehavior. + */ +void +ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) +{ + JsonExprState *jsestate = op->d.jsonexpr.jsestate; + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonExpr *jexpr = jsestate->jsexpr; + Datum item; + Datum res = (Datum) 0; + bool resnull = true; + JsonPath *path; + bool *error; + bool *empty; + + item = pre_eval->formatted_expr.value; + path = DatumGetJsonPathP(pre_eval->pathspec.value); + + /* Reset JsonExprPostEvalState for this evaluation. */ + memset(post_eval, 0, sizeof(*post_eval)); + + /* Respect ON ERROR behavior during path evaluation. */ + jsestate->throw_error = (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR); + + /* + * Be sure to save the error (if needed) and empty statuses for the + * EEOP_JSONEXPR_BEHAVIOR step to peruse. + */ + error = !jsestate->throw_error ? &post_eval->error : NULL; + empty = &post_eval->empty; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + res = JsonPathQuery(item, path, jexpr->wrapper, empty, error, + pre_eval->args); + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + resnull = !DatumGetPointer(res); + break; + + case JSON_VALUE_OP: + { + JsonbValue *jbv = JsonPathValue(item, path, empty, error, + pre_eval->args); + + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + if (!jbv) /* NULL or empty */ + { + resnull = true; + break; + } + + Assert(!*empty); + + resnull = false; + + /* coerce scalar item to the output type */ + if (jexpr->returning->typid == JSONOID || + jexpr->returning->typid == JSONBOID) + { + /* Use result coercion from json[b] to the output type */ + res = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + break; + } + + /* + * Error out if no cast exists to coerce SQL/JSON item to the + * the output type. + */ + Assert(post_eval->item_jcstate == NULL); + res = ExecPrepareJsonItemCoercion(jbv, + jsestate->item_jcstates, + &post_eval->item_jcstate); + if (post_eval->item_jcstate && + post_eval->item_jcstate->coercion && + (post_eval->item_jcstate->coercion->via_io || + post_eval->item_jcstate->coercion->via_populate)) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * Coercion via I/O means here that the cast to the target + * type simply does not exist. + */ + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE), + errmsg("SQL/JSON item cannot be cast to target type"))); + } + break; + } + + case JSON_EXISTS_OP: + { + bool exists = JsonPathExists(item, path, + pre_eval->args, + error); + + resnull = error && *error; + res = BoolGetDatum(exists); + break; + } + + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * If the ON EMPTY behavior is to cause an error, do so here. Other + * behaviors will be handled by the caller. + */ + if (*empty) + { + Assert(jexpr->on_empty); /* it is not JSON_EXISTS */ + + if (jexpr->on_empty->btype == JSON_BEHAVIOR_ERROR) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + ereport(ERROR, + (errcode(ERRCODE_NO_SQL_JSON_ITEM), + errmsg("no SQL/JSON item"))); + } + } + + *op->resvalue = res; + *op->resnull = resnull; +} + +/* + * Skip calling ExecEvalJson() on the given JsonExpr? + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_skip.jsestate; + + /* + * Skip if either of the input expressions has turned out to be NULL, + * though do execute domain checks for NULLs, which are handled by the + * coercion step. + */ + if (jsestate->pre_eval.formatted_expr.isnull || + jsestate->pre_eval.pathspec.isnull) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_skip.jump_coercion; + } + + /* + * Go evaluate the PASSING args if any and subsequently JSON path itself. + */ + return op->d.jsonexpr_skip.jump_passing_args; +} + +/* + * Returns the step address to perform the JsonBehavior applicable to + * the JSON item that resulted from evaluating the given JsonExpr. + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_behavior.jsestate; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonBehavior *behavior = NULL; + int jump_to = -1; + + if (post_eval->error || post_eval->coercion_error) + { + behavior = jsestate->jsexpr->on_error; + jump_to = op->d.jsonexpr_behavior.jump_onerror_expr; + } + else if (post_eval->empty) + { + behavior = jsestate->jsexpr->on_empty; + jump_to = op->d.jsonexpr_behavior.jump_onempty_expr; + } + else if (!post_eval->coercion_done) + { + /* + * If no error or the JSON item is not empty, directly go to the + * coercion step to coerce the item as is. + */ + return op->d.jsonexpr_behavior.jump_coercion; + } + + Assert(behavior); + + /* + * Set up for coercion step that will run to coerce a non-default behavior + * value. It should use result_coercion, if any. Errors that may occur + * should be thrown for JSON ops other than JSON_VALUE_OP. + */ + if (behavior->btype != JSON_BEHAVIOR_DEFAULT) + { + post_eval->item_jcstate = NULL; + jsestate->throw_error = (jsestate->jsexpr->op != JSON_VALUE_OP); + } + + Assert(jump_to >= 0); + return jump_to; +} + +/* + * Evaluate or return the step address to evaluate a coercion of a JSON item + * to the target type. The former if the coercion must be done right away by + * calling its input function or by calling json_populate_type(). + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull) +{ + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonExpr *jexpr = jsestate->jsexpr; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonCoercionState *item_jcstate = post_eval->item_jcstate; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + + if (item_jcstate == NULL && + jsestate->jsexpr->op != JSON_EXISTS_OP) + { + ErrorSaveContext escontext = {T_ErrorSaveContext}; + Node *escontext_p; + JsonCoercion *coercion; + Jsonb *jb; + + escontext_p = !jsestate->throw_error ? (Node *) &escontext : NULL; + coercion = result_jcstate ? result_jcstate->coercion : NULL; + jb = resnull ? NULL : DatumGetJsonbP(res); + + if ((coercion && coercion->via_io) || + (jexpr->omit_quotes && !resnull && + JB_ROOT_IS_SCALAR(jb))) + { + /* strip quotes and call typinput function */ + char *str = resnull ? NULL : JsonbUnquote(jb); + bool type_is_domain; + + type_is_domain = (getBaseType(jexpr->returning->typid) != + jexpr->returning->typid); + + /* + * Catch errors only if the type is not a domain, because errors + * caused by a domain's constraint failure must be thrown right + * away. + */ + if (!InputFunctionCallSafe(jsestate->input.finfo, str, + jsestate->input.typioparam, + jexpr->returning->typmod, + !type_is_domain ? escontext_p : NULL, + op->resvalue)) + { + post_eval->error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + else if (coercion && coercion->via_populate) + { + *op->resvalue = json_populate_type(res, JSONBOID, + jexpr->returning->typid, + jexpr->returning->typmod, + &post_eval->cache, + econtext->ecxt_per_query_memory, + op->resnull, + escontext_p); + if (SOFT_ERROR_OCCURRED(escontext_p)) + { + post_eval->error = true; + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + } + + if (!jsestate->throw_error) + { + post_eval->escontext.type = T_ErrorSaveContext; + state->escontext = (Node *) &post_eval->escontext; + } + + if (item_jcstate && item_jcstate->jump_eval_expr >= 0) + return item_jcstate->jump_eval_expr; + else if (result_jcstate && result_jcstate->jump_eval_expr >= 0) + return result_jcstate->jump_eval_expr; + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; +} + +/* + * Checks if the coercion evaluation led to an error. If an error did occur, + * this returns the address of the step that handles the error, otherwise + * the step after the coercion step, which finishes the JsonExpr processing. + */ +int +ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op) +{ + JsonExprPostEvalState *post_eval = + &op->d.jsonexpr_coercion_finish.jsestate->post_eval; + + if (SOFT_ERROR_OCCURRED(state->escontext)) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + post_eval->coercion_error = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_error; + } + + /* Reset. */ + state->escontext = NULL; + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_done; +} + +/* + * Prepare SQL/JSON item coercion to the output type. Returned a datum of the + * corresponding SQL type and a pointer to the coercion state. + */ +static Datum +ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate) +{ + JsonCoercionState *item_jcstate; + Datum res; + JsonbValue buf; + + if (item->type == jbvBinary && + JsonContainerIsScalar(item->val.binary.data)) + { + bool res PG_USED_FOR_ASSERTS_ONLY; + + res = JsonbExtractScalar(item->val.binary.data, &buf); + item = &buf; + Assert(res); + } + + /* get coercion state reference and datum of the corresponding SQL type */ + switch (item->type) + { + case jbvNull: + item_jcstate = item_jcstates->null; + res = (Datum) 0; + break; + + case jbvString: + item_jcstate = item_jcstates->string; + res = PointerGetDatum(cstring_to_text_with_len(item->val.string.val, + item->val.string.len)); + break; + + case jbvNumeric: + item_jcstate = item_jcstates->numeric; + res = NumericGetDatum(item->val.numeric); + break; + + case jbvBool: + item_jcstate = item_jcstates->boolean; + res = BoolGetDatum(item->val.boolean); + break; + + case jbvDatetime: + res = item->val.datetime.value; + switch (item->val.datetime.typid) + { + case DATEOID: + item_jcstate = item_jcstates->date; + break; + case TIMEOID: + item_jcstate = item_jcstates->time; + break; + case TIMETZOID: + item_jcstate = item_jcstates->timetz; + break; + case TIMESTAMPOID: + item_jcstate = item_jcstates->timestamp; + break; + case TIMESTAMPTZOID: + item_jcstate = item_jcstates->timestamptz; + break; + default: + elog(ERROR, "unexpected jsonb datetime type oid %u", + item->val.datetime.typid); + return (Datum) 0; + } + break; + + case jbvArray: + case jbvObject: + case jbvBinary: + item_jcstate = item_jcstates->composite; + res = JsonbPGetDatum(JsonbValueToJsonb(item)); + break; + + default: + elog(ERROR, "unexpected jsonb value type %d", item->type); + return (Datum) 0; + } + + *p_item_jcstate = item_jcstate; + + return res; +} + /* * ExecEvalGroupingFunc diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 00d7b8110b..da3870cba6 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -1860,6 +1860,256 @@ llvm_compile_expr(ExprState *state) LLVMBuildBr(b, opblocks[opno + 1]); break; + case EEOP_JSONEXPR_PATH: + build_EvalXFunc(b, mod, "ExecEvalJsonExpr", + v_state, op, v_econtext); + LLVMBuildBr(b, opblocks[opno + 1]); + break; + + case EEOP_JSONEXPR_SKIP: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprSkip() to decide if JSON path + * evaluation can be skipped. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprSkip"), + params, lengthof(params), ""); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_skip.jump_coercion, which signifies + * skipping of JSON path evaluation, else to the next step + * which must point to the steps to evaluate PASSING args, + * if any, or to the JSON path evaluation. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_skip.jump_coercion), + ""), + opblocks[op->d.jsonexpr_skip.jump_coercion], + opblocks[opno + 1]); + break; + } + + case EEOP_JSONEXPR_BEHAVIOR: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + LLVMBasicBlockRef b_jump_onerror_default; + + /* + * Call ExecEvalJsonExprBehavior() to decide if ON EMPTY + * or ON ERROR behavior must be invoked depending on what + * JSON path evaluation returned. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprBehavior"), + params, lengthof(params), ""); + + b_jump_onerror_default = + l_bb_before_v(opblocks[opno + 1], + "op.%d.jsonexpr_behavior_jump_onerror_default", opno); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_behavior.jump_coercion, else to the + * next block, one that checks whether to evaluate the ON + * ERROR default expression. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_coercion), + ""), + opblocks[op->d.jsonexpr_behavior.jump_coercion], + b_jump_onerror_default); + + /* + * Block that checks whether to evaluate the ON ERROR + * default expression. + * + * Jump to evaluate the ON ERROR default expression if the + * returned address is the same as + * jsonexpr_behavior.jump_onerror_default, else jump to + * evaluate the ON EMPTY default expression. + */ + LLVMPositionBuilderAtEnd(b, b_jump_onerror_default); + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_onerror_expr), + ""), + opblocks[op->d.jsonexpr_behavior.jump_onerror_expr], + opblocks[op->d.jsonexpr_behavior.jump_onempty_expr]); + break; + } + case EEOP_JSONEXPR_COERCION: + { + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonItemCoercionsState *item_jcstates = jsestate->item_jcstates; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + LLVMValueRef v_ret; + LLVMValueRef params[5]; + + /* + * Call ExecEvalJsonExprCoercion() to evaluate appropriate + * coercion. This will return the step address to jump + * to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + params[2] = v_econtext; + params[3] = v_resvaluep; + params[4] = v_resnullp; + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercion"), + params, lengthof(params), ""); + + /* + * Jump to handle a coercion error if the returned address + * is the same as jsonexpr_coercion.jump_coercion_error, + * else to the step after coercion (coercion done!). + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + + if (item_jcstates) + { + JsonCoercionState **item_jcstate; + int n_coercions = (int) + (item_jcstates->composite - item_jcstates->null) + 1; + int i; + LLVMBasicBlockRef *b_coercions; + + + /* + * Will create a block for each coercion below to + * check whether to evaluate the coercion's expression + * if there's one or to skip to the end if not. + */ + b_coercions = palloc((n_coercions + 1) * sizeof(LLVMBasicBlockRef)); + for (i = 0; i < n_coercions + 1; i++) + b_coercions[i] = + l_bb_before_v(opblocks[opno + 1], + "op.%d.json_item_coercion.%d", + opno, i); + + /* Jump to check first coercion */ + LLVMBuildBr(b, b_coercions[0]); + + /* + * Add conditional branches for individual coercion's + * expressions + */ + for (item_jcstate = &item_jcstates->null, i = 0; + item_jcstate <= &item_jcstates->composite; + item_jcstate++, i++) + { + /* Block for this coercion */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + + /* + * Jump to evaluate the coercion's expression if + * the address returned is the same as this + * coercion's jump_eval_expr (that is, if it is + * valid), else check the next coercion's. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const((*item_jcstate)->jump_eval_expr), + ""), + (*item_jcstate)->jump_eval_expr >= 0 ? + opblocks[(*item_jcstate)->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + b_coercions[i + 1]); + } + + /* + * A placeholder block that the last coercion's block + * might jump to, which unconditionally jumps to end + * of coercions. + */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + LLVMBuildBr(b, opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + + /* + * Jump to evaluate the result_coercion's expression if + * none of the above coercions matched, that is, if + * there's one. + */ + if (result_jcstate) + { + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(result_jcstate->jump_eval_expr), + ""), + result_jcstate->jump_eval_expr >= 0 ? + opblocks[result_jcstate->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + break; + } + + case EEOP_JSONEXPR_COERCION_FINISH: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprCoercionFinish() to check whether + * an coercion error occurred, in which case we must jump + * to whatever step handles the error. This returns the + * step address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercionFinish"), + params, lengthof(params), ""); + + /* + * Jump to the step that handles coercion error if the + * returned address is the same as + * jsonexpr_coercion_finish.jump_coercion_error, else to + * jsonexpr_coercion_finish.jump_coercion_done. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion_finish.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_done]); + break; + } + case EEOP_AGGREF: { LLVMValueRef v_aggno; diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c index 41ac4c6f45..cf3ced3427 100644 --- a/src/backend/jit/llvm/llvmjit_types.c +++ b/src/backend/jit/llvm/llvmjit_types.c @@ -135,6 +135,11 @@ void *referenced_functions[] = ExecEvalXmlExpr, ExecEvalJsonConstructor, ExecEvalJsonIsPredicate, + ExecEvalJsonExprSkip, + ExecEvalJsonExprBehavior, + ExecEvalJsonExprCoercion, + ExecEvalJsonExprCoercionFinish, + ExecEvalJsonExpr, MakeExpandedObjectReadOnlyInternal, slot_getmissingattrs, slot_getsomeattrs_int, diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 39e1884cf4..5dc3aa2e9f 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -859,6 +859,21 @@ makeJsonValueExpr(Expr *expr, JsonFormat *format) return jve; } +/* + * makeJsonBehavior - + * creates a JsonBehavior node + */ +JsonBehavior * +makeJsonBehavior(JsonBehaviorType type, Node *default_expr) +{ + JsonBehavior *behavior = makeNode(JsonBehavior); + + behavior->btype = type; + behavior->default_expr = default_expr; + + return behavior; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 0ed8712a63..7f48efbef1 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -236,6 +236,12 @@ exprType(const Node *expr) case T_JsonIsPredicate: type = BOOLOID; break; + case T_JsonExpr: + type = ((const JsonExpr *) expr)->returning->typid; + break; + case T_JsonCoercion: + type = exprType(((const JsonCoercion *) expr)->expr); + break; case T_NullTest: type = BOOLOID; break; @@ -495,6 +501,10 @@ exprTypmod(const Node *expr) return exprTypmod((Node *) ((const JsonValueExpr *) expr)->formatted_expr); case T_JsonConstructorExpr: return ((const JsonConstructorExpr *) expr)->returning->typmod; + case T_JsonExpr: + return ((JsonExpr *) expr)->returning->typmod; + case T_JsonCoercion: + return exprTypmod(((const JsonCoercion *) expr)->expr); case T_CoerceToDomain: return ((const CoerceToDomain *) expr)->resulttypmod; case T_CoerceToDomainValue: @@ -971,6 +981,22 @@ exprCollation(const Node *expr) /* IS JSON's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + coll = InvalidOid; + else if (coercion->expr) + coll = exprCollation(coercion->expr); + else if (coercion->via_io || coercion->via_populate) + coll = coercion->collation; + else + coll = InvalidOid; + } + break; + case T_NullTest: /* NullTest's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ @@ -1207,6 +1233,21 @@ exprSetCollation(Node *expr, Oid collation) case T_JsonIsPredicate: Assert(!OidIsValid(collation)); /* result is always boolean */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + Assert(!OidIsValid(collation)); + else if (coercion->expr) + exprSetCollation(coercion->expr, collation); + else if (coercion->via_io || coercion->via_populate) + coercion->collation = collation; + else + Assert(!OidIsValid(collation)); + } + break; case T_NullTest: /* NullTest's result is boolean ... */ Assert(!OidIsValid(collation)); /* ... so never set a collation */ @@ -1510,6 +1551,15 @@ exprLocation(const Node *expr) case T_JsonIsPredicate: loc = ((const JsonIsPredicate *) expr)->location; break; + case T_JsonExpr: + { + const JsonExpr *jsexpr = (const JsonExpr *) expr; + + /* consider both function name and leftmost arg */ + loc = leftmostLoc(jsexpr->location, + exprLocation(jsexpr->formatted_expr)); + } + break; case T_NullTest: { const NullTest *nexpr = (const NullTest *) expr; @@ -2262,6 +2312,54 @@ expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + if (WALK(jexpr->formatted_expr)) + return true; + if (WALK(jexpr->result_coercion)) + return true; + if (WALK(jexpr->passing_values)) + return true; + /* we assume walker doesn't care about passing_names */ + if (jexpr->on_empty && + WALK(jexpr->on_empty->default_expr)) + return true; + if (WALK(jexpr->on_error->default_expr)) + return true; + if (WALK(jexpr->coercions)) + return true; + } + break; + case T_JsonCoercion: + return WALK(((JsonCoercion *) node)->expr); + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + + if (WALK(coercions->null)) + return true; + if (WALK(coercions->string)) + return true; + if (WALK(coercions->numeric)) + return true; + if (WALK(coercions->boolean)) + return true; + if (WALK(coercions->date)) + return true; + if (WALK(coercions->time)) + return true; + if (WALK(coercions->timetz)) + return true; + if (WALK(coercions->timestamp)) + return true; + if (WALK(coercions->timestamptz)) + return true; + if (WALK(coercions->composite)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: @@ -3261,6 +3359,54 @@ expression_tree_mutator_impl(Node *node, return (Node *) newnode; } + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + JsonExpr *newnode; + + FLATCOPY(newnode, jexpr, JsonExpr); + MUTATE(newnode->path_spec, jexpr->path_spec, Node *); + MUTATE(newnode->formatted_expr, jexpr->formatted_expr, Node *); + MUTATE(newnode->result_coercion, jexpr->result_coercion, JsonCoercion *); + MUTATE(newnode->passing_values, jexpr->passing_values, List *); + /* assume mutator does not care about passing_names */ + if (newnode->on_empty) + MUTATE(newnode->on_empty->default_expr, + jexpr->on_empty->default_expr, Node *); + MUTATE(newnode->on_error->default_expr, + jexpr->on_error->default_expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonCoercion: + { + JsonCoercion *coercion = (JsonCoercion *) node; + JsonCoercion *newnode; + + FLATCOPY(newnode, coercion, JsonCoercion); + MUTATE(newnode->expr, coercion->expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + JsonItemCoercions *newnode; + + FLATCOPY(newnode, coercions, JsonItemCoercions); + MUTATE(newnode->null, coercions->null, JsonCoercion *); + MUTATE(newnode->string, coercions->string, JsonCoercion *); + MUTATE(newnode->numeric, coercions->numeric, JsonCoercion *); + MUTATE(newnode->boolean, coercions->boolean, JsonCoercion *); + MUTATE(newnode->date, coercions->date, JsonCoercion *); + MUTATE(newnode->time, coercions->time, JsonCoercion *); + MUTATE(newnode->timetz, coercions->timetz, JsonCoercion *); + MUTATE(newnode->timestamp, coercions->timestamp, JsonCoercion *); + MUTATE(newnode->timestamptz, coercions->timestamptz, JsonCoercion *); + MUTATE(newnode->composite, coercions->composite, JsonCoercion *); + return (Node *) newnode; + } + break; case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -3917,6 +4063,43 @@ raw_expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonArgument: + return WALK(((JsonArgument *) node)->val); + case T_JsonCommon: + { + JsonCommon *jc = (JsonCommon *) node; + + if (WALK(jc->expr)) + return true; + if (WALK(jc->pathspec)) + return true; + if (WALK(jc->passing)) + return true; + } + break; + case T_JsonBehavior: + { + JsonBehavior *jb = (JsonBehavior *) node; + + if (jb->btype == JSON_BEHAVIOR_DEFAULT && + WALK(jb->default_expr)) + return true; + } + break; + case T_JsonFuncExpr: + { + JsonFuncExpr *jfe = (JsonFuncExpr *) node; + + if (WALK(jfe->common)) + return true; + if (jfe->output && WALK(jfe->output)) + return true; + if (WALK(jfe->on_empty)) + return true; + if (WALK(jfe->on_error)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index ef475d95a1..f58c275b4b 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -4609,7 +4609,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context) IsA(node, SQLValueFunction) || IsA(node, XmlExpr) || IsA(node, CoerceToDomain) || - IsA(node, NextValueExpr)) + IsA(node, NextValueExpr) || + IsA(node, JsonExpr)) { /* Treat all these as having cost 1 */ context->total.per_tuple += cpu_operator_cost; diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 7f453b04f8..23a138571d 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -53,6 +53,7 @@ #include "utils/fmgroids.h" #include "utils/json.h" #include "utils/jsonb.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/syscache.h" @@ -412,6 +413,24 @@ contain_mutable_functions_walker(Node *node, void *context) /* Check all subnodes */ } + if (IsA(node, JsonExpr)) + { + JsonExpr *jexpr = castNode(JsonExpr, node); + Const *cnst; + + if (!IsA(jexpr->path_spec, Const)) + return true; + + cnst = castNode(Const, jexpr->path_spec); + + Assert(cnst->consttype == JSONPATHOID); + if (cnst->constisnull) + return false; + + return jspIsMutable(DatumGetJsonPathP(cnst->constvalue), + jexpr->passing_names, jexpr->passing_values); + } + if (IsA(node, SQLValueFunction)) { /* all variants of SQLValueFunction are stable */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 39ab7eac0d..c42abaa2d5 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -278,6 +278,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MergeWhenClause *mergewhen; struct KeyActions *keyactions; struct KeyAction *keyaction; + JsonBehavior *jsbehavior; + JsonQuotes js_quotes; } %type <node> stmt toplevel_stmt schema_stmt routine_body_stmt @@ -648,6 +650,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> json_format_clause_opt json_value_expr + json_api_common_syntax + json_argument + json_returning_clause_opt json_output_clause_opt json_name_and_value json_aggregate_func @@ -655,9 +660,20 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt + json_arguments + json_passing_clause_opt + +%type <str> json_as_path_name_clause_opt %type <ival> json_encoding_clause_opt json_predicate_type_constraint + json_wrapper_behavior + +%type <jsbehavior> json_value_behavior + json_query_behavior + json_exists_behavior + +%type <js_quotes> json_quotes_clause_opt %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt @@ -698,7 +714,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT - COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT + COMMITTED COMPRESSION CONCURRENTLY CONDITIONAL CONFIGURATION CONFLICT CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY COST CREATE CROSS CSV CUBE CURRENT_P CURRENT_CATALOG CURRENT_DATE CURRENT_ROLE CURRENT_SCHEMA @@ -709,8 +725,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P DOUBLE_P DROP - EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT - EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION + EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE + EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION EXTENSION EXTERNAL EXTRACT FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR @@ -725,9 +741,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION - JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG + JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG + JSON_QUERY JSON_VALUE - KEY KEYS + KEY KEYS KEEP LABEL LANGUAGE LARGE_P LAST_P LATERAL_P LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL @@ -741,7 +758,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC - OBJECT_P OF OFF OFFSET OIDS OLD ON ONLY OPERATOR OPTION OPTIONS OR + OBJECT_P OF OFF OFFSET OIDS OLD OMIT ON ONLY OPERATOR OPTION OPTIONS OR ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER @@ -750,7 +767,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION - QUOTE + QUOTE QUOTES RANGE READ REAL REASSIGN RECHECK RECURSIVE REF_P REFERENCES REFERENCING REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA @@ -761,7 +778,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); SEQUENCE SEQUENCES SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P - START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P + START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRING_P STRIP_P SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN @@ -769,7 +786,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); TREAT TRIGGER TRIM TRUE_P TRUNCATE TRUSTED TYPE_P TYPES_P - UESCAPE UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN + UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING @@ -15628,7 +15645,192 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } - ; + | JSON_QUERY '(' + json_api_common_syntax + json_output_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_output_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_output_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_error = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_output_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->on_error = $10; + n->location = @1; + $$ = (Node *) n; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->location = @1; + $$ = (Node *) p; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + json_exists_behavior ON ERROR_P + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->on_error = $5; + p->location = @1; + $$ = (Node *) p; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->on_error = $8; + n->location = @1; + $$ = (Node *) n; + } + ; /* * SQL/XML support @@ -16353,6 +16555,48 @@ opt_asymmetric: ASYMMETRIC ; /* SQL/JSON support */ +json_api_common_syntax: + json_value_expr ',' a_expr /* i.e. a json_path */ + json_as_path_name_clause_opt + json_passing_clause_opt + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $4; + n->passing = $5; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_as_path_name_clause_opt: + AS name { $$ = $2; } + | /* EMPTY */ { $$ = NULL; } + ; + +json_passing_clause_opt: + PASSING json_arguments { $$ = $2; } + | /* EMPTY */ { $$ = NIL; } + ; + +json_arguments: + json_argument { $$ = list_make1($1); } + | json_arguments ',' json_argument { $$ = lappend($1, $3); } + ; + +json_argument: + json_value_expr AS ColLabel + { + JsonArgument *n = makeNode(JsonArgument); + + n->val = (JsonValueExpr *) $1; + n->name = $3; + $$ = (Node *) n; + } + ; + json_value_expr: a_expr json_format_clause_opt { @@ -16376,6 +16620,64 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; +json_query_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + | EMPTY_P ARRAY { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + | EMPTY_P OBJECT_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + /* non-standard, for Oracle compatibility only */ + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + ; + +json_exists_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | TRUE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_TRUE, NULL); } + | FALSE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_FALSE, NULL); } + | UNKNOWN { $$ = makeJsonBehavior(JSON_BEHAVIOR_UNKNOWN, NULL); } + ; + +json_value_behavior: + NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + ; + +/* ARRAY is a noise word */ +json_wrapper_behavior: + WITHOUT WRAPPER { $$ = JSW_NONE; } + | WITHOUT ARRAY WRAPPER { $$ = JSW_NONE; } + | WITH WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL ARRAY WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL WRAPPER { $$ = JSW_UNCONDITIONAL; } + | /* empty */ { $$ = JSW_NONE; } + ; + +json_quotes_clause_opt: + KEEP QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_KEEP; } + | KEEP QUOTES { $$ = JS_QUOTES_KEEP; } + | OMIT QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_OMIT; } + | OMIT QUOTES { $$ = JS_QUOTES_OMIT; } + | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } + ; + +json_returning_clause_opt: + RETURNING Typename + { + JsonOutput *n = makeNode(JsonOutput); + + n->typeName = $2; + n->returning = makeNode(JsonReturning); + n->returning->format = + makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, @2); + $$ = (Node *) n; + } + | /* EMPTY */ { $$ = NULL; } + ; + json_output_clause_opt: RETURNING Typename json_format_clause_opt { @@ -16978,6 +17280,7 @@ unreserved_keyword: | COMMIT | COMMITTED | COMPRESSION + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17014,10 +17317,12 @@ unreserved_keyword: | DOUBLE_P | DROP | EACH + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17068,6 +17373,7 @@ unreserved_keyword: | INVOKER | ISOLATION | JSON + | KEEP | KEY | KEYS | LABEL @@ -17114,6 +17420,7 @@ unreserved_keyword: | OFF | OIDS | OLD + | OMIT | OPERATOR | OPTION | OPTIONS @@ -17144,6 +17451,7 @@ unreserved_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REASSIGN @@ -17203,6 +17511,7 @@ unreserved_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUPPORT @@ -17225,6 +17534,7 @@ unreserved_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNKNOWN | UNLISTEN @@ -17284,8 +17594,11 @@ col_name_keyword: | INTERVAL | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY + | JSON_VALUE | LEAST | NATIONAL | NCHAR @@ -17518,6 +17831,7 @@ bare_label_keyword: | COMMITTED | COMPRESSION | CONCURRENTLY + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17570,11 +17884,13 @@ bare_label_keyword: | DROP | EACH | ELSE + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | END_P | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17644,8 +17960,12 @@ bare_label_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY + | JSON_VALUE + | KEEP | KEY | KEYS | LABEL @@ -17706,6 +18026,7 @@ bare_label_keyword: | OFF | OIDS | OLD + | OMIT | ONLY | OPERATOR | OPTION @@ -17743,6 +18064,7 @@ bare_label_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REAL @@ -17811,6 +18133,7 @@ bare_label_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUBSTRING @@ -17845,6 +18168,7 @@ bare_label_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNIQUE | UNKNOWN diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c index 9f6afc351c..692e5d1225 100644 --- a/src/backend/parser/parse_collate.c +++ b/src/backend/parser/parse_collate.c @@ -691,6 +691,13 @@ assign_collations_walker(Node *node, assign_collations_context *context) &loccontext); } break; + case T_JsonExpr: + + /* + * Context item and PASSING arguments are already + * marked with collations in parse_expr.c. + */ + break; default: /* diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 346fd272b6..d7bf7e5b81 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -86,6 +86,8 @@ static Node *transformJsonArrayQueryConstructor(ParseState *pstate, static Node *transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg); static Node *transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg); static Node *transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred); +static Node *transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *p); +static Node *transformJsonValueExpr(ParseState *pstate, JsonValueExpr *jve); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -337,6 +339,14 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonIsPredicate(pstate, (JsonIsPredicate *) expr); break; + case T_JsonFuncExpr: + result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); + break; + + case T_JsonValueExpr: + result = transformJsonValueExpr(pstate, (JsonValueExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3222,8 +3232,8 @@ makeCaseTestExpr(Node *expr) * default format otherwise. */ static Node * -transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, - JsonFormatType default_format) +transformJsonValueExprExt(ParseState *pstate, JsonValueExpr *ve, + JsonFormatType default_format, bool isarg) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3246,6 +3256,8 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, get_type_category_preferred(exprtype, &typcategory, &typispreferred); + rawexpr = expr; + if (ve->format->format_type != JS_FORMAT_DEFAULT) { if (ve->format->encoding != JS_ENC_DEFAULT && exprtype != BYTEAOID) @@ -3264,12 +3276,44 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, else format = ve->format->format_type; } + else if (isarg) + { + /* Pass SQL/JSON item types directly without conversion to json[b]. */ + switch (exprtype) + { + case TEXTOID: + case NUMERICOID: + case BOOLOID: + case INT2OID: + case INT4OID: + case INT8OID: + case FLOAT4OID: + case FLOAT8OID: + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + return expr; + + default: + if (typcategory == TYPCATEGORY_STRING) + return coerce_to_specific_type(pstate, expr, TEXTOID, + "JSON_VALUE_EXPR"); + /* else convert argument to json[b] type */ + break; + } + + format = default_format; + } else if (exprtype == JSONOID || exprtype == JSONBOID) format = JS_FORMAT_DEFAULT; /* do not format json[b] types */ else format = default_format; - if (format != JS_FORMAT_DEFAULT) + if (format == JS_FORMAT_DEFAULT) + expr = rawexpr; + else { Oid targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; Node *orig = makeCaseTestExpr(expr); @@ -3277,7 +3321,7 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, expr = orig; - if (exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) + if (!isarg && exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), errmsg(ve->format->format_type == JS_FORMAT_DEFAULT ? @@ -3329,6 +3373,24 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, return expr; } +/* + * Transform JSON value expression using FORMAT JSON by default. + */ +static Node * +transformJsonValueExpr(ParseState *pstate, JsonValueExpr *jve) +{ + return transformJsonValueExprExt(pstate, jve, JS_FORMAT_JSON, false); +} + +/* + * Transform JSON value expression using unspecified format by default. + */ +static Node * +transformJsonValueExprDefault(ParseState *pstate, JsonValueExpr *jve) +{ + return transformJsonValueExprExt(pstate, jve, JS_FORMAT_DEFAULT, false); +} + /* * Checks specified output format for its applicability to the target type. */ @@ -3588,8 +3650,7 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) { JsonKeyValue *kv = castNode(JsonKeyValue, lfirst(lc)); Node *key = transformExprRecurse(pstate, (Node *) kv->key); - Node *val = transformJsonValueExpr(pstate, kv->value, - JS_FORMAT_DEFAULT); + Node *val = transformJsonValueExprDefault(pstate, kv->value); args = lappend(args, key); args = lappend(args, val); @@ -3768,7 +3829,7 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) Oid aggtype; key = transformExprRecurse(pstate, (Node *) agg->arg->key); - val = transformJsonValueExpr(pstate, agg->arg->value, JS_FORMAT_DEFAULT); + val = transformJsonValueExprDefault(pstate, agg->arg->value); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3824,7 +3885,7 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) Oid aggfnoid; Oid aggtype; - arg = transformJsonValueExpr(pstate, agg->arg, JS_FORMAT_DEFAULT); + arg = transformJsonValueExprDefault(pstate, agg->arg); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3870,8 +3931,7 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) foreach(lc, ctor->exprs) { JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); - Node *val = transformJsonValueExpr(pstate, jsval, - JS_FORMAT_DEFAULT); + Node *val = transformJsonValueExprDefault(pstate, jsval); args = lappend(args, val); } @@ -3954,3 +4014,416 @@ transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred) return makeJsonIsPredicate(expr, NULL, pred->item_type, pred->unique_keys, pred->location); } + +/* + * Transform a JSON PASSING clause. + */ +static void +transformJsonPassingArgs(ParseState *pstate, JsonFormatType format, List *args, + List **passing_values, List **passing_names) +{ + ListCell *lc; + + *passing_values = NIL; + *passing_names = NIL; + + foreach(lc, args) + { + JsonArgument *arg = castNode(JsonArgument, lfirst(lc)); + Node *expr = transformJsonValueExprExt(pstate, arg->val, + format, true); + + assign_expr_collations(pstate, expr); + + *passing_values = lappend(*passing_values, expr); + *passing_names = lappend(*passing_names, makeString(arg->name)); + } +} + +/* + * Transform a JSON BEHAVIOR clause. + */ +static JsonBehavior * +transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior, + JsonBehaviorType default_behavior) +{ + JsonBehaviorType behavior_type = default_behavior; + Node *default_expr = NULL; + + if (behavior) + { + behavior_type = behavior->btype; + if (behavior_type == JSON_BEHAVIOR_DEFAULT) + default_expr = transformExprRecurse(pstate, behavior->default_expr); + } + return makeJsonBehavior(behavior_type, default_expr); +} + +/* + * Common code for JSON_VALUE, JSON_QUERY, JSON_EXISTS transformation + * into a JsonExpr node. + */ +static JsonExpr * +transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = makeNode(JsonExpr); + Node *pathspec; + JsonFormatType format; + + if (func->common->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("JSON_TABLE path name is not allowed here"), + parser_errposition(pstate, func->location))); + + jsexpr->location = func->location; + jsexpr->op = func->op; + jsexpr->formatted_expr = transformJsonValueExpr(pstate, func->common->expr); + + assign_expr_collations(pstate, jsexpr->formatted_expr); + + /* format is determined by context item type */ + format = exprType(jsexpr->formatted_expr) == JSONBOID ? JS_FORMAT_JSONB : JS_FORMAT_JSON; + + jsexpr->result_coercion = NULL; + jsexpr->omit_quotes = false; + + jsexpr->format = func->common->expr->format; + + pathspec = transformExprRecurse(pstate, func->common->pathspec); + + jsexpr->path_spec = + coerce_to_target_type(pstate, pathspec, exprType(pathspec), + JSONPATHOID, -1, + COERCION_EXPLICIT, COERCE_IMPLICIT_CAST, + exprLocation(pathspec)); + if (!jsexpr->path_spec) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("JSON path expression must be type %s, not type %s", + "jsonpath", format_type_be(exprType(pathspec))), + parser_errposition(pstate, exprLocation(pathspec)))); + + /* transform and coerce to json[b] passing arguments */ + transformJsonPassingArgs(pstate, format, func->common->passing, + &jsexpr->passing_values, &jsexpr->passing_names); + + if (func->op != JSON_EXISTS_OP) + jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, + JSON_BEHAVIOR_NULL); + + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + func->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : + JSON_BEHAVIOR_NULL); + + return jsexpr; +} + +/* + * Assign default JSON returning type from the specified format or from + * the context item type. + */ +static void +assignDefaultJsonReturningType(Node *context_item, JsonFormat *context_format, + JsonReturning *ret) +{ + bool is_jsonb; + + ret->format = copyObject(context_format); + + if (ret->format->format_type == JS_FORMAT_DEFAULT) + is_jsonb = exprType(context_item) == JSONBOID; + else + is_jsonb = ret->format->format_type == JS_FORMAT_JSONB; + + ret->typid = is_jsonb ? JSONBOID : JSONOID; + ret->typmod = -1; +} + +/* + * Try to coerce expression to the output type or + * use json_populate_type() for composite, array and domain types or + * use coercion via I/O. + */ +static JsonCoercion * +coerceJsonExpr(ParseState *pstate, Node *expr, const JsonReturning *returning) +{ + char typtype; + JsonCoercion *coercion = makeNode(JsonCoercion); + + coercion->expr = coerceJsonFuncExpr(pstate, expr, returning, false); + + if (coercion->expr) + { + if (coercion->expr == expr) + coercion->expr = NULL; + + return coercion; + } + + typtype = get_typtype(returning->typid); + + if (returning->typid == RECORDOID || + typtype == TYPTYPE_COMPOSITE || + typtype == TYPTYPE_DOMAIN || + type_is_array(returning->typid)) + coercion->via_populate = true; + else + coercion->via_io = true; + + return coercion; +} + +/* + * Transform a JSON output clause of JSON_VALUE and JSON_QUERY. + */ +static void +transformJsonFuncExprOutput(ParseState *pstate, JsonFuncExpr *func, + JsonExpr *jsexpr) +{ + Node *expr = jsexpr->formatted_expr; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + /* JSON_VALUE returns text by default */ + if (func->op == JSON_VALUE_OP && !OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = TEXTOID; + jsexpr->returning->typmod = -1; + } + + if (OidIsValid(jsexpr->returning->typid)) + { + JsonReturning ret; + + if (func->op == JSON_VALUE_OP && + jsexpr->returning->typid != JSONOID && + jsexpr->returning->typid != JSONBOID) + { + /* Forced coercion via I/O for JSON_VALUE for non-JSON types */ + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = NULL; + jsexpr->result_coercion->via_io = true; + return; + } + + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, &ret); + + if (ret.typid != jsexpr->returning->typid || + ret.typmod != jsexpr->returning->typmod) + { + Node *placeholder = makeCaseTestExpr(expr); + + Assert(((CaseTestExpr *) placeholder)->typeId == ret.typid); + Assert(((CaseTestExpr *) placeholder)->typeMod == ret.typmod); + + jsexpr->result_coercion = coerceJsonExpr(pstate, placeholder, + jsexpr->returning); + } + } + else + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, + jsexpr->returning); +} + +/* + * Coerce an expression in JSON DEFAULT behavior to the target output type. + */ +static Node * +coerceDefaultJsonExpr(ParseState *pstate, JsonExpr *jsexpr, Node *defexpr) +{ + int location; + Oid exprtype; + + if (!defexpr) + return NULL; + + exprtype = exprType(defexpr); + location = exprLocation(defexpr); + + if (location < 0) + location = jsexpr->location; + + defexpr = coerce_to_target_type(pstate, + defexpr, + exprtype, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!defexpr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast DEFAULT expression type %s to %s", + format_type_be(exprtype), + format_type_be(jsexpr->returning->typid)), + parser_errposition(pstate, location))); + + return defexpr; +} + +/* + * Initialize SQL/JSON item coercion from the SQL type "typid" to the target + * "returning" type. + */ +static JsonCoercion * +initJsonItemCoercion(ParseState *pstate, Oid typid, + const JsonReturning *returning) +{ + Node *expr; + + if (typid == UNKNOWNOID) + { + expr = (Node *) makeNullConst(UNKNOWNOID, -1, InvalidOid); + } + else + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + + placeholder->typeId = typid; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + expr = (Node *) placeholder; + } + + return coerceJsonExpr(pstate, expr, returning); +} + +static void +initJsonItemCoercions(ParseState *pstate, JsonItemCoercions *coercions, + const JsonReturning *returning, Oid contextItemTypeId) +{ + struct + { + JsonCoercion **coercion; + Oid typid; + } *p, + coercionTypids[] = + { + {&coercions->null, UNKNOWNOID}, + {&coercions->string, TEXTOID}, + {&coercions->numeric, NUMERICOID}, + {&coercions->boolean, BOOLOID}, + {&coercions->date, DATEOID}, + {&coercions->time, TIMEOID}, + {&coercions->timetz, TIMETZOID}, + {&coercions->timestamp, TIMESTAMPOID}, + {&coercions->timestamptz, TIMESTAMPTZOID}, + {&coercions->composite, contextItemTypeId}, + {NULL, InvalidOid} + }; + + for (p = coercionTypids; p->coercion; p++) + *p->coercion = initJsonItemCoercion(pstate, p->typid, returning); +} + +/* + * Transform JSON_VALUE, JSON_QUERY, JSON_EXISTS functions into a JsonExpr node. + */ +static Node * +transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = transformJsonExprCommon(pstate, func); + const char *func_name = NULL; + Node *contextItemExpr = jsexpr->formatted_expr; + + switch (func->op) + { + case JSON_VALUE_OP: + func_name = "JSON_VALUE"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->coercions = makeNode(JsonItemCoercions); + initJsonItemCoercions(pstate, jsexpr->coercions, jsexpr->returning, + exprType(contextItemExpr)); + + break; + + case JSON_QUERY_OP: + func_name = "JSON_QUERY"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->wrapper = func->wrapper; + jsexpr->omit_quotes = func->omit_quotes; + + break; + + case JSON_EXISTS_OP: + func_name = "JSON_EXISTS"; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + if (!OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = BOOLOID; + jsexpr->returning->typmod = -1; + } + else if (jsexpr->returning->typid != BOOLOID) + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + int location = exprLocation((Node *) jsexpr); + + placeholder->typeId = BOOLOID; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = + coerce_to_target_type(pstate, (Node *) placeholder, BOOLOID, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!jsexpr->result_coercion->expr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(BOOLOID), + format_type_be(jsexpr->returning->typid)), + parser_coercion_errposition(pstate, location, (Node *) jsexpr))); + + if (jsexpr->result_coercion->expr == (Node *) placeholder) + jsexpr->result_coercion->expr = NULL; + } + break; + } + + if (exprType(contextItemExpr) != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("%s() is not yet implemented for the json type", func_name), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + return (Node *) jsexpr; +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4cca97ff9c..8aa47d3307 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1970,6 +1970,21 @@ FigureColnameInternal(Node *node, char **name) /* make JSON_ARRAYAGG act like a regular function */ *name = "json_arrayagg"; return 2; + case T_JsonFuncExpr: + /* make SQL/JSON functions act like a regular function */ + switch (((JsonFuncExpr *) node)->op) + { + case JSON_QUERY_OP: + *name = "json_query"; + return 2; + case JSON_VALUE_OP: + *name = "json_value"; + return 2; + case JSON_EXISTS_OP: + *name = "json_exists"; + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index e6246dc44b..f6015debbb 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -4444,6 +4444,50 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, } } +/* + * Parses the datetime format string in 'fmt_str' and returns true if it + * contains a timezone specifier, false if not. + */ +bool +datetime_format_has_tz(const char *fmt_str) +{ + bool incache; + int fmt_len = strlen(fmt_str); + int result; + FormatNode *format; + + if (fmt_len > DCH_CACHE_SIZE) + { + /* + * Allocate new memory if format picture is bigger than static cache + * and do not use cache (call parser always) + */ + incache = false; + + format = (FormatNode *) palloc((fmt_len + 1) * sizeof(FormatNode)); + + parse_format(format, fmt_str, DCH_keywords, + DCH_suff, DCH_index, DCH_FLAG, NULL); + } + else + { + /* + * Use cache buffers + */ + DCHCacheEntry *ent = DCH_cache_fetch(fmt_str, false); + + incache = true; + format = ent->format; + } + + result = DCH_datetime_type(format); + + if (!incache) + pfree(format); + + return result & DCH_ZONED; +} + /* * do_to_timestamp: shared code for to_timestamp and to_date * diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index cf43c3f2de..4b7007b482 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -2257,3 +2257,65 @@ jsonb_float8(PG_FUNCTION_ARGS) PG_RETURN_DATUM(retValue); } + +/* + * Construct an empty array jsonb. + */ +Jsonb * +JsonbMakeEmptyArray(void) +{ + JsonbValue jbv; + + jbv.type = jbvArray; + jbv.val.array.elems = NULL; + jbv.val.array.nElems = 0; + jbv.val.array.rawScalar = false; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Construct an empty object jsonb. + */ +Jsonb * +JsonbMakeEmptyObject(void) +{ + JsonbValue jbv; + + jbv.type = jbvObject; + jbv.val.object.pairs = NULL; + jbv.val.object.nPairs = 0; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Convert jsonb to a C-string stripping quotes from scalar strings. + */ +char * +JsonbUnquote(Jsonb *jb) +{ + if (JB_ROOT_IS_SCALAR(jb)) + { + JsonbValue v; + + (void) JsonbExtractScalar(&jb->root, &v); + + if (v.type == jbvString) + return pnstrdup(v.val.string.val, v.val.string.len); + else if (v.type == jbvBool) + return pstrdup(v.val.boolean ? "true" : "false"); + else if (v.type == jbvNumeric) + return DatumGetCString(DirectFunctionCall1(numeric_out, + PointerGetDatum(v.val.numeric))); + else if (v.type == jbvNull) + return pstrdup("null"); + else + { + elog(ERROR, "unrecognized jsonb value type %d", v.type); + return NULL; + } + } + else + return JsonbToCString(NULL, &jb->root, VARSIZE(jb)); +} diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index 70cb922e6b..cbc6b94fc2 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -264,6 +264,7 @@ typedef struct PopulateArrayContext int *dims; /* dimensions */ int *sizes; /* current dimension counters */ int ndims; /* number of dimensions */ + Node *escontext; /* For soft-error capture */ } PopulateArrayContext; /* state for populate_array_json() */ @@ -441,12 +442,13 @@ static void JsValueToJsObject(JsValue *jsv, JsObject *jso); static Datum populate_composite(CompositeIOData *io, Oid typid, const char *colname, MemoryContext mcxt, HeapTupleHeader defaultval, JsValue *jsv, bool isnull); -static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv); +static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext); static void prepare_column_cache(ColumnIOData *column, Oid typid, int32 typmod, MemoryContext mcxt, bool need_scalar); static Datum populate_record_field(ColumnIOData *col, Oid typid, int32 typmod, const char *colname, MemoryContext mcxt, Datum defaultval, - JsValue *jsv, bool *isnull); + JsValue *jsv, bool *isnull, Node *escontext); static RecordIOData *allocate_record_info(MemoryContext mcxt, int ncolumns); static bool JsObjectGetField(JsObject *obj, char *field, JsValue *jsv); static void populate_recordset_record(PopulateRecordsetState *state, JsObject *obj); @@ -458,7 +460,8 @@ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims); static void populate_array_check_dimension(PopulateArrayContext *ctx, int ndim); static void populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv); static Datum populate_array(ArrayIOData *aio, const char *colname, - MemoryContext mcxt, JsValue *jsv); + MemoryContext mcxt, JsValue *jsv, Node *escontext, + bool *isnull); static Datum populate_domain(DomainIOData *io, Oid typid, const char *colname, MemoryContext mcxt, JsValue *jsv, bool isnull); @@ -2483,12 +2486,12 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) if (ndim <= 0) { if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the value of key \"%s\".", ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"))); } @@ -2505,13 +2508,13 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) appendStringInfo(&indices, "[%d]", ctx->sizes[i]); if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s of key \"%s\".", indices.data, ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s.", @@ -2519,7 +2522,11 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) } } -/* set the number of dimensions of the populated array when it becomes known */ +/* + * Set the number of dimensions of the populated array when it becomes known. + * + * Returns without doing anything if the input (ndims) is erratic. + */ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) { @@ -2530,6 +2537,10 @@ populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) if (ndims <= 0) populate_array_report_expected_array(ctx, ndims); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + ctx->ndims = ndims; ctx->dims = palloc(sizeof(int) * ndims); ctx->sizes = palloc0(sizeof(int) * ndims); @@ -2547,12 +2558,16 @@ populate_array_check_dimension(PopulateArrayContext *ctx, int ndim) if (ctx->dims[ndim] == -1) ctx->dims[ndim] = dim; /* assign dimension if not yet known */ else if (ctx->dims[ndim] != dim) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("malformed JSON array"), errdetail("Multidimensional arrays must have " "sub-arrays with matching dimensions."))); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + /* reset the current array dimension size counter */ ctx->sizes[ndim] = 0; @@ -2572,7 +2587,10 @@ populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv) ctx->aio->element_type, ctx->aio->element_typmod, NULL, ctx->mcxt, PointerGetDatum(NULL), - jsv, &element_isnull); + jsv, &element_isnull, ctx->escontext); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; accumArrayResult(ctx->astate, element, element_isnull, ctx->aio->element_type, ctx->acxt); @@ -2593,6 +2611,10 @@ populate_array_object_start(void *_state) else if (ndim < state->ctx->ndims) populate_array_report_expected_array(state->ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2610,6 +2632,10 @@ populate_array_array_end(void *_state) if (ndim < ctx->ndims) populate_array_check_dimension(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2685,6 +2711,10 @@ populate_array_scalar(void *_state, char *token, JsonTokenType tokentype) else if (ndim < ctx->ndims) populate_array_report_expected_array(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + if (ndim == ctx->ndims) { /* remember the scalar element token */ @@ -2714,7 +2744,13 @@ populate_array_json(PopulateArrayContext *ctx, char *json, int len) sem.array_element_end = populate_array_element_end; sem.scalar = populate_array_scalar; - pg_parse_json_or_ereport(state.lex, &sem); + if (!pg_parse_json_or_errsave(state.lex, &sem, ctx->escontext)) + { + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + pfree(state.lex); + return; + } /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2739,10 +2775,15 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ check_stack_depth(); - if (jbv->type != jbvBinary || !JsonContainerIsArray(jbc)) + if (jbv->type != jbvBinary || + !JsonContainerIsArray(jbc) || + JsonContainerIsScalar(jbc)) + { populate_array_report_expected_array(ctx, ndim - 1); - - Assert(!JsonContainerIsScalar(jbc)); + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + return; + } it = JsonbIteratorInit(jbc); @@ -2761,7 +2802,12 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ (tok == WJB_ELEM && (val.type != jbvBinary || !JsonContainerIsArray(val.val.binary.data))))) + { populate_array_assign_ndims(ctx, ndim); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + } jsv.is_json = false; jsv.val.jsonb = &val; @@ -2779,6 +2825,9 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ { /* populate child sub-array */ populate_array_dim_jsonb(ctx, &val, ndim + 1); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2796,12 +2845,18 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ Assert(tok == WJB_DONE && !it); } -/* recursively populate an array from json/jsonb */ +/* + * Recursively populate an array from json/jsonb + * + * *isnull is set to true if an error is reported during parsing. + */ static Datum populate_array(ArrayIOData *aio, const char *colname, MemoryContext mcxt, - JsValue *jsv) + JsValue *jsv, + Node *escontext, + bool *isnull) { PopulateArrayContext ctx; Datum result; @@ -2816,6 +2871,7 @@ populate_array(ArrayIOData *aio, ctx.ndims = 0; /* unknown yet */ ctx.dims = NULL; ctx.sizes = NULL; + ctx.escontext = escontext; if (jsv->is_json) populate_array_json(&ctx, jsv->val.json.str, @@ -2824,7 +2880,16 @@ populate_array(ArrayIOData *aio, else { populate_array_dim_jsonb(&ctx, jsv->val.jsonb, 1); - ctx.dims[0] = ctx.sizes[0]; + /* Nothing to do on an error. */ + if (!SOFT_ERROR_OCCURRED(ctx.escontext)) + ctx.dims[0] = ctx.sizes[0]; + } + + /* Nothing to return if an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx.escontext)) + { + *isnull = true; + return (Datum) 0; } Assert(ctx.ndims > 0); @@ -2841,6 +2906,7 @@ populate_array(ArrayIOData *aio, pfree(ctx.sizes); pfree(lbs); + *isnull = false; return result; } @@ -2956,7 +3022,8 @@ populate_composite(CompositeIOData *io, /* populate non-null scalar value from json/jsonb value */ static Datum -populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) +populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext) { Datum res; char *str = NULL; @@ -3027,7 +3094,11 @@ populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) elog(ERROR, "unrecognized jsonb type: %d", (int) jbv->type); } - res = InputFunctionCall(&io->typiofunc, str, io->typioparam, typmod); + if (!InputFunctionCallSafe(&io->typiofunc, str, io->typioparam, typmod, + escontext, &res)) + { + res = (Datum) 0; + } /* free temporary buffer */ if (str != json) @@ -3053,7 +3124,7 @@ populate_domain(DomainIOData *io, res = populate_record_field(io->base_io, io->base_typid, io->base_typmod, colname, mcxt, PointerGetDatum(NULL), - jsv, &isnull); + jsv, &isnull, NULL); Assert(!isnull); } @@ -3158,7 +3229,8 @@ populate_record_field(ColumnIOData *col, MemoryContext mcxt, Datum defaultval, JsValue *jsv, - bool *isnull) + bool *isnull, + Node *escontext) { TypeCat typcat; @@ -3191,10 +3263,12 @@ populate_record_field(ColumnIOData *col, switch (typcat) { case TYPECAT_SCALAR: - return populate_scalar(&col->scalar_io, typid, typmod, jsv); + return populate_scalar(&col->scalar_io, typid, typmod, jsv, + escontext); case TYPECAT_ARRAY: - return populate_array(&col->io.array, colname, mcxt, jsv); + return populate_array(&col->io.array, colname, mcxt, jsv, + escontext, isnull); case TYPECAT_COMPOSITE: case TYPECAT_COMPOSITE_DOMAIN: @@ -3215,6 +3289,53 @@ populate_record_field(ColumnIOData *col, } } +/* recursively populate specified type from a json/jsonb value */ +Datum +json_populate_type(Datum json_val, Oid json_type, Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext) +{ + JsValue jsv = {0}; + JsonbValue jbv; + + jsv.is_json = json_type == JSONOID; + + if (*isnull) + { + if (jsv.is_json) + jsv.val.json.str = NULL; + else + jsv.val.jsonb = NULL; + } + else if (jsv.is_json) + { + text *json = DatumGetTextPP(json_val); + + jsv.val.json.str = VARDATA_ANY(json); + jsv.val.json.len = VARSIZE_ANY_EXHDR(json); + jsv.val.json.type = JSON_TOKEN_INVALID; /* not used in + * populate_composite() */ + } + else + { + Jsonb *jsonb = DatumGetJsonbP(json_val); + + jsv.val.jsonb = &jbv; + + /* fill binary jsonb value pointing to jb */ + jbv.type = jbvBinary; + jbv.val.binary.data = &jsonb->root; + jbv.val.binary.len = VARSIZE(jsonb) - VARHDRSZ; + } + + if (!*cache) + *cache = MemoryContextAllocZero(mcxt, sizeof(ColumnIOData)); + + return populate_record_field(*cache, typid, typmod, NULL, mcxt, + PointerGetDatum(NULL), &jsv, isnull, + escontext); +} + static RecordIOData * allocate_record_info(MemoryContext mcxt, int ncolumns) { @@ -3356,7 +3477,8 @@ populate_record(TupleDesc tupdesc, mcxt, nulls[i] ? (Datum) 0 : values[i], &field, - &nulls[i]); + &nulls[i], + NULL); } res = heap_form_tuple(tupdesc, values, nulls); diff --git a/src/backend/utils/adt/jsonpath.c b/src/backend/utils/adt/jsonpath.c index 7891fde310..5194d0d91f 100644 --- a/src/backend/utils/adt/jsonpath.c +++ b/src/backend/utils/adt/jsonpath.c @@ -68,7 +68,9 @@ #include "libpq/pqformat.h" #include "nodes/miscnodes.h" #include "miscadmin.h" +#include "nodes/nodeFuncs.h" #include "utils/builtins.h" +#include "utils/formatting.h" #include "utils/json.h" #include "utils/jsonpath.h" @@ -1109,3 +1111,256 @@ jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, return true; } + +/* SQL/JSON datatype status: */ +typedef enum JsonPathDatatypeStatus +{ + jpdsNonDateTime, /* null, bool, numeric, string, array, object */ + jpdsUnknownDateTime, /* unknown datetime type */ + jpdsDateTimeZoned, /* timetz, timestamptz */ + jpdsDateTimeNonZoned /* time, timestamp, date */ +} JsonPathDatatypeStatus; + +/* Context for jspIsMutableWalker() */ +typedef struct JsonPathMutableContext +{ + List *varnames; /* list of variable names */ + List *varexprs; /* list of variable expressions */ + JsonPathDatatypeStatus current; /* status of @ item */ + bool lax; /* jsonpath is lax or strict */ + bool mutable; /* resulting mutability status */ +} JsonPathMutableContext; + +/* + * Recursive walker for jspIsMutable() + */ +static JsonPathDatatypeStatus +jspIsMutableWalker(JsonPathItem *jpi, JsonPathMutableContext *cxt) +{ + JsonPathItem next; + JsonPathDatatypeStatus status = jpdsNonDateTime; + + while (!cxt->mutable) + { + JsonPathItem arg; + JsonPathDatatypeStatus leftStatus; + JsonPathDatatypeStatus rightStatus; + + switch (jpi->type) + { + case jpiRoot: + Assert(status == jpdsNonDateTime); + break; + + case jpiCurrent: + Assert(status == jpdsNonDateTime); + status = cxt->current; + break; + + case jpiFilter: + { + JsonPathDatatypeStatus prevStatus = cxt->current; + + cxt->current = status; + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + + cxt->current = prevStatus; + break; + } + + case jpiVariable: + { + int32 len; + const char *name = jspGetString(jpi, &len); + ListCell *lc1; + ListCell *lc2; + + Assert(status == jpdsNonDateTime); + + forboth(lc1, cxt->varnames, lc2, cxt->varexprs) + { + String *varname = lfirst_node(String, lc1); + Node *varexpr = lfirst(lc2); + + if (strncmp(varname->sval, name, len)) + continue; + + switch (exprType(varexpr)) + { + case DATEOID: + case TIMEOID: + case TIMESTAMPOID: + status = jpdsDateTimeNonZoned; + break; + + case TIMETZOID: + case TIMESTAMPTZOID: + status = jpdsDateTimeZoned; + break; + + default: + status = jpdsNonDateTime; + break; + } + + break; + } + break; + } + + case jpiEqual: + case jpiNotEqual: + case jpiLess: + case jpiGreater: + case jpiLessOrEqual: + case jpiGreaterOrEqual: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + leftStatus = jspIsMutableWalker(&arg, cxt); + + jspGetRightArg(jpi, &arg); + rightStatus = jspIsMutableWalker(&arg, cxt); + + /* + * Comparison of datetime type with different timezone status + * is mutable. + */ + if (leftStatus != jpdsNonDateTime && + rightStatus != jpdsNonDateTime && + (leftStatus == jpdsUnknownDateTime || + rightStatus == jpdsUnknownDateTime || + leftStatus != rightStatus)) + cxt->mutable = true; + break; + + case jpiNot: + case jpiIsUnknown: + case jpiExists: + case jpiPlus: + case jpiMinus: + Assert(status == jpdsNonDateTime); + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiAnd: + case jpiOr: + case jpiAdd: + case jpiSub: + case jpiMul: + case jpiDiv: + case jpiMod: + case jpiStartsWith: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + jspGetRightArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiIndexArray: + for (int i = 0; i < jpi->content.array.nelems; i++) + { + JsonPathItem from; + JsonPathItem to; + + if (jspGetArraySubscript(jpi, &from, &to, i)) + jspIsMutableWalker(&to, cxt); + + jspIsMutableWalker(&from, cxt); + } + /* FALLTHROUGH */ + + case jpiAnyArray: + if (!cxt->lax) + status = jpdsNonDateTime; + break; + + case jpiAny: + if (jpi->content.anybounds.first > 0) + status = jpdsNonDateTime; + break; + + case jpiDatetime: + if (jpi->content.arg) + { + char *template; + + jspGetArg(jpi, &arg); + if (arg.type != jpiString) + { + status = jpdsNonDateTime; + break; /* there will be runtime error */ + } + + template = jspGetString(&arg, NULL); + if (datetime_format_has_tz(template)) + status = jpdsDateTimeZoned; + else + status = jpdsDateTimeNonZoned; + } + else + { + status = jpdsUnknownDateTime; + } + break; + + case jpiLikeRegex: + Assert(status == jpdsNonDateTime); + jspInitByBuffer(&arg, jpi->base, jpi->content.like_regex.expr); + jspIsMutableWalker(&arg, cxt); + break; + + /* literals */ + case jpiNull: + case jpiString: + case jpiNumeric: + case jpiBool: + /* accessors */ + case jpiKey: + case jpiAnyKey: + /* special items */ + case jpiSubscript: + case jpiLast: + /* item methods */ + case jpiType: + case jpiSize: + case jpiAbs: + case jpiFloor: + case jpiCeiling: + case jpiDouble: + case jpiKeyValue: + status = jpdsNonDateTime; + break; + } + + if (!jspGetNext(jpi, &next)) + break; + + jpi = &next; + } + + return status; +} + +/* + * Check whether jsonpath expression is immutable or not. + */ +bool +jspIsMutable(JsonPath *path, List *varnames, List *varexprs) +{ + JsonPathMutableContext cxt; + JsonPathItem jpi; + + cxt.varnames = varnames; + cxt.varexprs = varexprs; + cxt.current = jpdsNonDateTime; + cxt.lax = (path->header & JSONPATH_LAX) != 0; + cxt.mutable = false; + + jspInit(&jpi, path); + jspIsMutableWalker(&jpi, &cxt); + + return cxt.mutable; +} diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index 2d0599b4aa..eded22e194 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -87,12 +87,16 @@ typedef struct JsonBaseObjectInfo int id; } JsonBaseObjectInfo; +typedef int (*JsonPathVarCallback) (void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); + /* * Context of jsonpath execution. */ typedef struct JsonPathExecContext { - Jsonb *vars; /* variables to substitute into jsonpath */ + void *vars; /* variables to substitute into jsonpath */ + JsonPathVarCallback getVar; JsonbValue *root; /* for $ evaluation */ JsonbValue *current; /* for @ evaluation */ JsonBaseObjectInfo baseObject; /* "base object" for .keyvalue() @@ -174,7 +178,8 @@ typedef JsonPathBool (*JsonPathPredicateCallback) (JsonPathItem *jsp, void *param); typedef Numeric (*BinaryArithmFunc) (Numeric num1, Numeric num2, bool *error); -static JsonPathExecResult executeJsonPath(JsonPath *path, Jsonb *vars, +static JsonPathExecResult executeJsonPath(JsonPath *path, void *vars, + JsonPathVarCallback getVar, Jsonb *json, bool throwErrors, JsonValueList *result, bool useTz); static JsonPathExecResult executeItem(JsonPathExecContext *cxt, @@ -225,8 +230,13 @@ static JsonPathExecResult appendBoolResult(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonValueList *found, JsonPathBool res); static void getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, JsonbValue *value); +static int GetJsonPathVar(void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); static void getJsonPathVariable(JsonPathExecContext *cxt, - JsonPathItem *variable, Jsonb *vars, JsonbValue *value); + JsonPathItem *variable, JsonbValue *value); +static int getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, + int varNameLen, JsonbValue *val, + JsonbValue *baseObject); static int JsonbArraySize(JsonbValue *jb); static JsonPathBool executeComparison(JsonPathItem *cmp, JsonbValue *lv, JsonbValue *rv, void *p); @@ -284,7 +294,8 @@ jsonb_path_exists_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - res = executeJsonPath(jp, vars, jb, !silent, NULL, tz); + res = executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, NULL, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -339,7 +350,8 @@ jsonb_path_match_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -417,7 +429,8 @@ jsonb_path_query_internal(FunctionCallInfo fcinfo, bool tz) vars = PG_GETARG_JSONB_P_COPY(2); silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); funcctx->user_fctx = JsonValueListGetList(&found); @@ -464,7 +477,8 @@ jsonb_path_query_array_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_RETURN_JSONB_P(JsonbValueToJsonb(wrapItemsInArray(&found))); } @@ -495,7 +509,8 @@ jsonb_path_query_first_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); if (JsonValueListLength(&found) >= 1) PG_RETURN_JSONB_P(JsonbValueToJsonb(JsonValueListHead(&found))); @@ -537,8 +552,9 @@ jsonb_path_query_first_tz(PG_FUNCTION_ARGS) * In other case it tries to find all the satisfied result items. */ static JsonPathExecResult -executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, - JsonValueList *result, bool useTz) +executeJsonPath(JsonPath *path, void *vars, JsonPathVarCallback getVar, + Jsonb *json, bool throwErrors, JsonValueList *result, + bool useTz) { JsonPathExecContext cxt; JsonPathExecResult res; @@ -550,22 +566,16 @@ executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, if (!JsonbExtractScalar(&json->root, &jbv)) JsonbInitBinary(&jbv, json); - if (vars && !JsonContainerIsObject(&vars->root)) - { - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"vars\" argument is not an object"), - errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); - } - cxt.vars = vars; + cxt.getVar = getVar; cxt.laxMode = (path->header & JSONPATH_LAX) != 0; cxt.ignoreStructuralErrors = cxt.laxMode; cxt.root = &jbv; cxt.current = &jbv; cxt.baseObject.jbc = NULL; cxt.baseObject.id = 0; - cxt.lastGeneratedObjectId = vars ? 2 : 1; + /* 1 + number of base objects in vars */ + cxt.lastGeneratedObjectId = 1 + getVar(vars, NULL, 0, NULL, NULL); cxt.innermostArraySize = -1; cxt.throwErrors = throwErrors; cxt.useTz = useTz; @@ -2108,54 +2118,118 @@ getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, &value->val.string.len); break; case jpiVariable: - getJsonPathVariable(cxt, item, cxt->vars, value); + getJsonPathVariable(cxt, item, value); return; default: elog(ERROR, "unexpected jsonpath item type"); } } +/* + * Returns the computed value of a JSON path variable with given name. + */ +static int +GetJsonPathVar(void *cxt, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject) +{ + JsonPathVariable *var = NULL; + List *vars = cxt; + ListCell *lc; + int id = 1; + + if (!varName) + return list_length(vars); + + foreach(lc, vars) + { + JsonPathVariable *curvar = lfirst(lc); + + if (!strncmp(curvar->name, varName, varNameLen)) + { + var = curvar; + break; + } + + id++; + } + + if (!var) + return -1; + + if (var->isnull) + { + val->type = jbvNull; + return 0; + } + + JsonItemFromDatum(var->value, var->typid, var->typmod, val); + + *baseObject = *val; + return id; +} + /* * Get the value of variable passed to jsonpath executor */ static void getJsonPathVariable(JsonPathExecContext *cxt, JsonPathItem *variable, - Jsonb *vars, JsonbValue *value) + JsonbValue *value) { char *varName; int varNameLength; + JsonbValue baseObject; + int baseObjectId; + + Assert(variable->type == jpiVariable); + varName = jspGetString(variable, &varNameLength); + + if (!cxt->vars || + (baseObjectId = cxt->getVar(cxt->vars, varName, varNameLength, value, + &baseObject)) < 0) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("could not find jsonpath variable \"%s\"", + pnstrdup(varName, varNameLength)))); + + if (baseObjectId > 0) + setBaseObject(cxt, &baseObject, baseObjectId); +} + +static int +getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, int varNameLength, + JsonbValue *value, JsonbValue *baseObject) +{ + Jsonb *vars = varsJsonb; JsonbValue tmp; JsonbValue *v; - if (!vars) + if (!varName) { - value->type = jbvNull; - return; + if (vars && !JsonContainerIsObject(&vars->root)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"vars\" argument is not an object"), + errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); + } + + return vars ? 1 : 0; /* count of base objects */ } - Assert(variable->type == jpiVariable); - varName = jspGetString(variable, &varNameLength); tmp.type = jbvString; tmp.val.string.val = varName; tmp.val.string.len = varNameLength; v = findJsonbValueFromContainer(&vars->root, JB_FOBJECT, &tmp); - if (v) - { - *value = *v; - pfree(v); - } - else - { - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("could not find jsonpath variable \"%s\"", - pnstrdup(varName, varNameLength)))); - } + if (!v) + return -1; - JsonbInitBinary(&tmp, vars); - setBaseObject(cxt, &tmp, 1); + *value = *v; + pfree(v); + + JsonbInitBinary(baseObject, vars); + return 1; } /**************** Support functions for JsonPath execution *****************/ @@ -2812,3 +2886,240 @@ compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, return DatumGetInt32(DirectFunctionCall2(cmpfunc, val1, val2)); } + +/* Executor-callable JSON_EXISTS implementation */ +bool +JsonPathExists(Datum jb, JsonPath *jp, List *vars, bool *error) +{ + JsonPathExecResult res = executeJsonPath(jp, vars, GetJsonPathVar, + DatumGetJsonbP(jb), !error, NULL, + true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + *error = true; + + return res == jperOk; +} + +/* Executor-callable JSON_QUERY implementation */ +Datum +JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, bool *empty, + bool *error, List *vars) +{ + JsonbValue *first; + bool wrap; + JsonValueList found = {0}; + JsonPathExecResult res PG_USED_FOR_ASSERTS_ONLY; + int count; + + res = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + { + *error = true; + *empty = false; + return (Datum) 0; + } + + count = JsonValueListLength(&found); + + first = count ? JsonValueListHead(&found) : NULL; + + if (!first) + wrap = false; + else if (wrapper == JSW_NONE) + wrap = false; + else if (wrapper == JSW_UNCONDITIONAL) + wrap = true; + else if (wrapper == JSW_CONDITIONAL) + wrap = count > 1 || + IsAJsonbScalar(first) || + (first->type == jbvBinary && + JsonContainerIsScalar(first->val.binary.data)); + else + { + elog(ERROR, "unrecognized json wrapper %d", wrapper); + wrap = false; + } + + if (wrap) + return JsonbPGetDatum(JsonbValueToJsonb(wrapItemsInArray(&found))); + + if (count > 1) + { + if (error) + { + *error = true; + return (Datum) 0; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_QUERY should return singleton item without wrapper"), + errhint("Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array."))); + } + + if (first) + return JsonbPGetDatum(JsonbValueToJsonb(first)); + + *empty = true; + return PointerGetDatum(NULL); +} + +/* Executor-callable JSON_VALUE implementation */ +JsonbValue * +JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars) +{ + JsonbValue *res; + JsonValueList found = {0}; + JsonPathExecResult jper PG_USED_FOR_ASSERTS_ONLY; + int count; + + jper = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(jper)); + + if (error && jperIsError(jper)) + { + *error = true; + *empty = false; + return NULL; + } + + count = JsonValueListLength(&found); + + *empty = !count; + + if (*empty) + return NULL; + + if (count > 1) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + res = JsonValueListHead(&found); + + if (res->type == jbvBinary && + JsonContainerIsScalar(res->val.binary.data)) + JsonbExtractScalar(res->val.binary.data, res); + + if (!IsAJsonbScalar(res)) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_SCALAR_REQUIRED), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + if (res->type == jbvNull) + return NULL; + + return res; +} + +static void +JsonbValueInitNumericDatum(JsonbValue *jbv, Datum num) +{ + jbv->type = jbvNumeric; + jbv->val.numeric = DatumGetNumeric(num); +} + +void +JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) +{ + switch (typid) + { + case BOOLOID: + res->type = jbvBool; + res->val.boolean = DatumGetBool(val); + break; + case NUMERICOID: + JsonbValueInitNumericDatum(res, val); + break; + case INT2OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int2_numeric, val)); + break; + case INT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int4_numeric, val)); + break; + case INT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int8_numeric, val)); + break; + case FLOAT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float4_numeric, val)); + break; + case FLOAT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float8_numeric, val)); + break; + case TEXTOID: + case VARCHAROID: + res->type = jbvString; + res->val.string.val = VARDATA_ANY(val); + res->val.string.len = VARSIZE_ANY_EXHDR(val); + break; + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + res->type = jbvDatetime; + res->val.datetime.value = val; + res->val.datetime.typid = typid; + res->val.datetime.typmod = typmod; + res->val.datetime.tz = 0; + break; + case JSONBOID: + { + JsonbValue *jbv = res; + Jsonb *jb = DatumGetJsonbP(val); + + if (JsonContainerIsScalar(&jb->root)) + { + bool result PG_USED_FOR_ASSERTS_ONLY; + + result = JsonbExtractScalar(&jb->root, jbv); + Assert(result); + } + else + JsonbInitBinary(jbv, jb); + break; + } + case JSONOID: + { + text *txt = DatumGetTextP(val); + char *str = text_to_cstring(txt); + Jsonb *jb; + + jb = DatumGetJsonbP(DirectFunctionCall1(jsonb_in, + CStringGetDatum(str))); + pfree(str); + + JsonItemFromDatum(JsonbPGetDatum(jb), JSONBOID, -1, res); + break; + } + default: + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); + } +} diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d3a973d86b..1d415e165c 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -476,6 +476,8 @@ static void get_const_expr(Const *constval, deparse_context *context, int showtype); static void get_const_collation(Const *constval, deparse_context *context); static void get_json_format(JsonFormat *format, StringInfo buf); +static void get_json_returning(JsonReturning *returning, StringInfo buf, + bool json_format_by_default); static void get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, bool showimplicit); static void get_json_constructor_options(JsonConstructorExpr *ctor, @@ -518,6 +520,8 @@ static char *generate_qualified_type_name(Oid typid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); +static void get_json_path_spec(Node *path_spec, deparse_context *context, + bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8279,6 +8283,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_WindowFunc: case T_FuncExpr: case T_JsonConstructorExpr: + case T_JsonExpr: /* function-like: name(..) or name[..] */ return true; @@ -8450,6 +8455,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_GroupingFunc: /* own parentheses */ case T_WindowFunc: /* own parentheses */ case T_CaseExpr: /* other separators */ + case T_JsonExpr: /* own parentheses */ return true; default: return false; @@ -8565,6 +8571,65 @@ get_rule_expr_paren(Node *node, deparse_context *context, appendStringInfoChar(context->buf, ')'); } +static void +get_json_behavior(JsonBehavior *behavior, deparse_context *context, + const char *on) +{ + /* + * The order of array elements must correspond to the order of + * JsonBehaviorType members. + */ + const char *behavior_names[] = + { + " NULL", + " ERROR", + " EMPTY", + " TRUE", + " FALSE", + " UNKNOWN", + " EMPTY ARRAY", + " EMPTY OBJECT", + " DEFAULT " + }; + + if ((int) behavior->btype < 0 || behavior->btype >= lengthof(behavior_names)) + elog(ERROR, "invalid json behavior type: %d", behavior->btype); + + appendStringInfoString(context->buf, behavior_names[behavior->btype]); + + if (behavior->btype == JSON_BEHAVIOR_DEFAULT) + get_rule_expr(behavior->default_expr, context, false); + + appendStringInfo(context->buf, " ON %s", on); +} + +/* + * get_json_expr_options + * + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + */ +static void +get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, + JsonBehaviorType default_behavior) +{ + if (jsexpr->op == JSON_QUERY_OP) + { + if (jsexpr->wrapper == JSW_CONDITIONAL) + appendStringInfo(context->buf, " WITH CONDITIONAL WRAPPER"); + else if (jsexpr->wrapper == JSW_UNCONDITIONAL) + appendStringInfo(context->buf, " WITH UNCONDITIONAL WRAPPER"); + + if (jsexpr->omit_quotes) + appendStringInfo(context->buf, " OMIT QUOTES"); + } + + if (jsexpr->op != JSON_EXISTS_OP && + jsexpr->on_empty->btype != default_behavior) + get_json_behavior(jsexpr->on_empty, context, "EMPTY"); + + if (jsexpr->on_error->btype != default_behavior) + get_json_behavior(jsexpr->on_error, context, "ERROR"); +} /* ---------- * get_rule_expr - Parse back an expression @@ -9724,6 +9789,7 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonValueExpr: { JsonValueExpr *jve = (JsonValueExpr *) node; @@ -9773,6 +9839,63 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + appendStringInfoString(buf, "JSON_QUERY("); + break; + case JSON_VALUE_OP: + appendStringInfoString(buf, "JSON_VALUE("); + break; + case JSON_EXISTS_OP: + appendStringInfoString(buf, "JSON_EXISTS("); + break; + } + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_json_path_spec(jexpr->path_spec, context, showimplicit); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoString(buf, " PASSING "); + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + get_rule_expr((Node *) lfirst(lc2), context, showimplicit); + appendStringInfo(buf, " AS %s", + ((String *) lfirst_node(String, lc1))->sval); + } + } + + if (jexpr->op != JSON_EXISTS_OP || + jexpr->returning->typid != BOOLOID) + get_json_returning(jexpr->returning, context->buf, + jexpr->op == JSON_QUERY_OP); + + get_json_expr_options(jexpr, context, + jexpr->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : JSON_BEHAVIOR_NULL); + + appendStringInfoString(buf, ")"); + } + break; + case T_List: { char *sep; @@ -9896,6 +10019,7 @@ looks_like_function(Node *node) case T_MinMaxExpr: case T_SQLValueFunction: case T_XmlExpr: + case T_JsonExpr: /* these are all accepted by func_expr_common_subexpr */ return true; default: @@ -10755,6 +10879,18 @@ get_const_collation(Const *constval, deparse_context *context) } } +/* + * get_json_path_spec - Parse back a JSON path specification + */ +static void +get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit) +{ + if (IsA(path_spec, Const)) + get_const_expr((Const *) path_spec, context, -1); + else + get_rule_expr(path_spec, context, showimplicit); +} + /* * get_json_format - Parse back a JsonFormat node */ diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index 048573c2bc..69fb577850 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -16,12 +16,15 @@ #include "executor/nodeAgg.h" #include "nodes/execnodes.h" +#include "nodes/miscnodes.h" /* forward references to avoid circularity */ struct ExprEvalStep; struct SubscriptingRefState; struct ScalarArrayOpExprHashTable; struct JsonConstructorExprState; +struct JsonbValue; +struct JsonExprState; /* Bits in ExprState->flags (see also execnodes.h for public flag bits): */ /* expression's interpreter has been initialized */ @@ -238,6 +241,11 @@ typedef enum ExprEvalOp EEOP_XMLEXPR, EEOP_JSON_CONSTRUCTOR, EEOP_IS_JSON, + EEOP_JSONEXPR_SKIP, + EEOP_JSONEXPR_PATH, + EEOP_JSONEXPR_BEHAVIOR, + EEOP_JSONEXPR_COERCION, + EEOP_JSONEXPR_COERCION_FINISH, EEOP_AGGREF, EEOP_GROUPING_FUNC, EEOP_WINDOW_FUNC, @@ -689,6 +697,57 @@ typedef struct ExprEvalStep JsonIsPredicate *pred; /* original expression node */ } is_json; + /* for EEOP_JSONEXPR_PATH */ + struct + { + struct JsonExprState *jsestate; + } jsonexpr; + + /* for EEOP_JSONEXPR_SKIP */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprSkip() */ + int jump_coercion; + int jump_passing_args; + } jsonexpr_skip; + + /* for EEOP_JSONEXPR_BEHAVIOR */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprBehavior() */ + int jump_onerror_expr; + int jump_onempty_expr; + int jump_coercion; + int jump_skip_coercion; + } jsonexpr_behavior; + + /* for EEOP_JSONEXPR_COERCION */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion; + + /* for EEOP_JSONEXPR_COERCION_FINISH */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion_finish; } d; } ExprEvalStep; @@ -752,6 +811,111 @@ typedef struct JsonConstructorExprState int nargs; } JsonConstructorExprState; +/* + * Information computed before evaluating a JsonExpr expression. + */ +typedef struct JsonExprPreEvalState +{ + /* value/isnull for JsonExpr.formatted_expr */ + NullableDatum formatted_expr; + + /* value/isnull for JsonExpr.pathspec */ + NullableDatum pathspec; + + /* JsonPathVariable entries for JsonExpr.passing_values */ + List *args; +} JsonExprPreEvalState; + +/* + * State for a given member of JsonItemCoercions. + */ +typedef struct JsonCoercionState +{ + /* Expression used to evaluate the coercion */ + JsonCoercion *coercion; + + /* ExprEvalStep to compute this coercion's expression */ + int jump_eval_expr; +} JsonCoercionState; + +/* + * State for evaluating the coercion for a given JSON item using one of + * the following coercions. + * + * Note that while ExecInitExprRec() for JsonItemCoercions will initialize + * ExprEvalSteps for all of the members that need it, only one will get run + * during a given evaluation of the enclosing JsonExpr depending on the type + * of the result JSON item. + */ +typedef struct JsonItemCoercionsState +{ + JsonCoercionState *null; + JsonCoercionState *string; + JsonCoercionState *numeric; + JsonCoercionState *boolean; + JsonCoercionState *date; + JsonCoercionState *time; + JsonCoercionState *timetz; + JsonCoercionState *timestamp; + JsonCoercionState *timestamptz; + JsonCoercionState *composite; +} JsonItemCoercionsState; + +/* + * State for some post-JsonExpr-evaluation processing steps that gets filled + * in JsonExpr evaluation. + */ +typedef struct JsonExprPostEvalState +{ + /* Is JSON item empty? */ + bool empty; + + /* Did JSON item evaluation cause an error? */ + bool error; + + /* Cache for json_populate_type() called for coercion in some cases */ + void *cache; + + /* + * State for evaluating a JSON item coercion. Points to one of those in + * JsonExprState.coercions chosen by ExecPrepareJsonItemCoercion(). + */ + JsonCoercionState *item_jcstate; + bool coercion_error; + bool coercion_done; + + ErrorSaveContext escontext; +} JsonExprPostEvalState; + +/* EEOP_JSONEXPR state, too big to inline */ +typedef struct JsonExprState +{ + /* original expression node */ + JsonExpr *jsexpr; + + /* + * Should errors be thrown or handled softly? Different parts of + * evaluating a given JsonExpr may require different treatment depending + * on the JsonExprOp; see ExecEvalJsonExprBehavior(). + */ + bool throw_error; + + JsonExprPreEvalState pre_eval; + JsonExprPostEvalState post_eval; + + struct + { + FmgrInfo *finfo; /* typinput function for output type */ + Oid typioparam; + } input; /* I/O info for output type */ + + /* + * Either of the following two is used by ExecEvalJsonExprCoercion() to + * apply coercion to the final result if needed. + */ + JsonCoercionState *result_jcstate; + JsonItemCoercionsState *item_jcstates; +} JsonExprState; /* functions in execExpr.c */ extern void ExprEvalPushStep(ExprState *es, const ExprEvalStep *s); @@ -805,6 +969,14 @@ extern void ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op); extern void ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull); +extern int ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op); +extern void ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, + ExprContext *econtext); extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op); extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op, ExprContext *econtext); diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index ac02247947..089d1c5750 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -17,6 +17,7 @@ #include "executor/execdesc.h" #include "fmgr.h" #include "nodes/lockoptions.h" +#include "nodes/miscnodes.h" #include "nodes/parsenodes.h" #include "utils/memutils.h" diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index cb714f4a19..584bf7001a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -129,6 +129,9 @@ typedef struct ExprState Datum *innermost_domainval; bool *innermost_domainnull; + + /* ErrorSaveContext for soft-error capture. */ + Node *escontext; } ExprState; diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 06d991b725..5e149b0266 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -111,6 +111,7 @@ extern VacuumRelation *makeVacuumRelation(RangeVar *relation, Oid oid, List *va_ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *expr, JsonFormat *format); +extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index b3bec90e52..a109106594 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1715,6 +1715,23 @@ typedef struct TriggerTransition /* Nodes for SQL/JSON support */ +/* + * JsonQuotes - + * representation of [KEEP|OMIT] QUOTES clause for JSON_QUERY() + */ +typedef enum JsonQuotes +{ + JS_QUOTES_UNSPEC, /* unspecified */ + JS_QUOTES_KEEP, /* KEEP QUOTES */ + JS_QUOTES_OMIT /* OMIT QUOTES */ +} JsonQuotes; + +/* + * JsonPathSpec - + * representation of JSON path constant + */ +typedef char *JsonPathSpec; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1726,6 +1743,48 @@ typedef struct JsonOutput JsonReturning *returning; /* RETURNING FORMAT clause and type Oids */ } JsonOutput; +/* + * JsonArgument - + * representation of argument from JSON PASSING clause + */ +typedef struct JsonArgument +{ + NodeTag type; + JsonValueExpr *val; /* argument value expression */ + char *name; /* argument name */ +} JsonArgument; + +/* + * JsonCommon - + * representation of common syntax of functions using JSON path + */ +typedef struct JsonCommon +{ + NodeTag type; + JsonValueExpr *expr; /* context item expression */ + Node *pathspec; /* JSON path specification expression */ + char *pathname; /* path name, if any */ + List *passing; /* list of PASSING clause arguments, if any */ + int location; /* token location, or -1 if unknown */ +} JsonCommon; + +/* + * JsonFuncExpr - + * untransformed representation of JSON function expressions + */ +typedef struct JsonFuncExpr +{ + NodeTag type; + JsonExprOp op; /* expression type */ + JsonCommon *common; /* common syntax */ + JsonOutput *output; /* output clause, if specified */ + JsonBehavior *on_empty; /* ON EMPTY behavior, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + JsonWrapper wrapper; /* array wrapper behavior (JSON_QUERY only) */ + bool omit_quotes; /* omit or keep quotes? (JSON_QUERY only) */ + int location; /* token location, or -1 if unknown */ +} JsonFuncExpr; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 792a743f72..9db0b847ff 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1542,6 +1542,17 @@ typedef struct XmlExpr int location; } XmlExpr; +/* + * JsonExprOp - + * enumeration of JSON functions using JSON path + */ +typedef enum JsonExprOp +{ + JSON_VALUE_OP, /* JSON_VALUE() */ + JSON_QUERY_OP, /* JSON_QUERY() */ + JSON_EXISTS_OP /* JSON_EXISTS() */ +} JsonExprOp; + /* * JsonEncoding - * representation of JSON ENCODING clause @@ -1566,6 +1577,37 @@ typedef enum JsonFormatType * jsonb */ } JsonFormatType; +/* + * JsonBehaviorType - + * enumeration of behavior types used in JSON ON ... BEHAVIOR clause + * + * If enum members are reordered, get_json_behavior() from ruleutils.c + * must be updated accordingly. + */ +typedef enum JsonBehaviorType +{ + JSON_BEHAVIOR_NULL = 0, + JSON_BEHAVIOR_ERROR, + JSON_BEHAVIOR_EMPTY, + JSON_BEHAVIOR_TRUE, + JSON_BEHAVIOR_FALSE, + JSON_BEHAVIOR_UNKNOWN, + JSON_BEHAVIOR_EMPTY_ARRAY, + JSON_BEHAVIOR_EMPTY_OBJECT, + JSON_BEHAVIOR_DEFAULT +} JsonBehaviorType; + +/* + * JsonWrapper - + * representation of WRAPPER clause for JSON_QUERY() + */ +typedef enum JsonWrapper +{ + JSW_NONE, + JSW_CONDITIONAL, + JSW_UNCONDITIONAL, +} JsonWrapper; + /* * JsonFormat - * representation of JSON FORMAT clause @@ -1653,6 +1695,73 @@ typedef struct JsonIsPredicate int location; /* token location, or -1 if unknown */ } JsonIsPredicate; +/* + * JsonBehavior - + * representation of JSON ON ... BEHAVIOR clause + */ +typedef struct JsonBehavior +{ + NodeTag type; + JsonBehaviorType btype; /* behavior type */ + Node *default_expr; /* default expression, if any */ +} JsonBehavior; + +/* + * JsonCoercion - + * coercion from SQL/JSON item types to SQL types + */ +typedef struct JsonCoercion +{ + NodeTag type; + Node *expr; /* resulting expression coerced to target type */ + bool via_populate; /* coerce result using json_populate_type()? */ + bool via_io; /* coerce result using type input function? */ + Oid collation; /* collation for coercion via I/O or populate */ +} JsonCoercion; + +/* + * JsonItemCoercions - + * expressions for coercion from SQL/JSON item types directly to the + * output SQL type + */ +typedef struct JsonItemCoercions +{ + NodeTag type; + JsonCoercion *null; + JsonCoercion *string; + JsonCoercion *numeric; + JsonCoercion *boolean; + JsonCoercion *date; + JsonCoercion *time; + JsonCoercion *timetz; + JsonCoercion *timestamp; + JsonCoercion *timestamptz; + JsonCoercion *composite; /* arrays and objects */ +} JsonItemCoercions; + +/* + * JsonExpr - + * transformed representation of JSON_VALUE(), JSON_QUERY(), JSON_EXISTS() + */ +typedef struct JsonExpr +{ + Expr xpr; + JsonExprOp op; /* json function ID */ + Node *formatted_expr; /* formatted context item expression */ + JsonCoercion *result_coercion; /* resulting coercion to RETURNING type */ + JsonFormat *format; /* context item format (JSON/JSONB) */ + Node *path_spec; /* JSON path specification expression */ + List *passing_names; /* PASSING argument names */ + List *passing_values; /* PASSING argument values */ + JsonReturning *returning; /* RETURNING clause type/format info */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + JsonItemCoercions *coercions; /* coercions for JSON_VALUE */ + JsonWrapper wrapper; /* WRAPPER for JSON_QUERY */ + bool omit_quotes; /* KEEP/OMIT QUOTES for JSON_QUERY */ + int location; /* token location, or -1 if unknown */ +} JsonExpr; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index f5b2e61ca5..b5556e331a 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -93,6 +93,7 @@ PG_KEYWORD("commit", COMMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("committed", COMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("compression", COMPRESSION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("concurrently", CONCURRENTLY, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("conditional", CONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("configuration", CONFIGURATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("conflict", CONFLICT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("connection", CONNECTION, UNRESERVED_KEYWORD, BARE_LABEL) @@ -147,11 +148,13 @@ PG_KEYWORD("double", DOUBLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("drop", DROP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("each", EACH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("empty", EMPTY_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("event", EVENT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("except", EXCEPT, RESERVED_KEYWORD, AS_LABEL) @@ -233,8 +236,12 @@ PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json", JSON, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_exists", JSON_EXISTS, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) @@ -300,6 +307,7 @@ PG_KEYWORD("off", OFF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("offset", OFFSET, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("oids", OIDS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("old", OLD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("omit", OMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("on", ON, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("only", ONLY, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("operator", OPERATOR, UNRESERVED_KEYWORD, BARE_LABEL) @@ -342,6 +350,7 @@ PG_KEYWORD("procedures", PROCEDURES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("publication", PUBLICATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("quotes", QUOTES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("range", RANGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("read", READ, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("real", REAL, COL_NAME_KEYWORD, BARE_LABEL) @@ -412,6 +421,7 @@ PG_KEYWORD("stdout", STDOUT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("storage", STORAGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("stored", STORED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("string", STRING_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("subscription", SUBSCRIPTION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("substring", SUBSTRING, COL_NAME_KEYWORD, BARE_LABEL) @@ -447,6 +457,7 @@ PG_KEYWORD("types", TYPES_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uescape", UESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unbounded", UNBOUNDED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uncommitted", UNCOMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("unconditional", UNCONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unencrypted", UNENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("union", UNION, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("unique", UNIQUE, RESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/formatting.h b/src/include/utils/formatting.h index 0cad3a2709..d4ca0f42ff 100644 --- a/src/include/utils/formatting.h +++ b/src/include/utils/formatting.h @@ -17,7 +17,6 @@ #ifndef _FORMATTING_H_ #define _FORMATTING_H_ - extern char *str_tolower(const char *buff, size_t nbytes, Oid collid); extern char *str_toupper(const char *buff, size_t nbytes, Oid collid); extern char *str_initcap(const char *buff, size_t nbytes, Oid collid); @@ -29,5 +28,6 @@ extern char *asc_initcap(const char *buff, size_t nbytes); extern Datum parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, Oid *typid, int32 *typmod, int *tz, struct Node *escontext); +extern bool datetime_format_has_tz(const char *fmt_str); #endif diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index 649a1644f2..ac279ee535 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -422,6 +422,9 @@ extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, int estimated_len); +extern Jsonb *JsonbMakeEmptyArray(void); +extern Jsonb *JsonbMakeEmptyObject(void); +extern char *JsonbUnquote(Jsonb *jb); extern bool JsonbExtractScalar(JsonbContainer *jbc, JsonbValue *res); extern const char *JsonbTypeName(JsonbValue *val); diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index a85203d4a4..cd9cc1480e 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -15,6 +15,7 @@ #define JSONFUNCS_H #include "common/jsonapi.h" +#include "nodes/nodes.h" #include "utils/jsonb.h" /* @@ -63,4 +64,9 @@ extern Jsonb *transform_jsonb_string_values(Jsonb *jsonb, void *action_state, extern text *transform_json_string_values(text *json, void *action_state, JsonTransformStringValuesAction transform_action); +extern Datum json_populate_type(Datum json_val, Oid json_type, + Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext); + #endif diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index f0181e045f..4dae78a98c 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -16,7 +16,9 @@ #include "fmgr.h" #include "nodes/pg_list.h" +#include "nodes/primnodes.h" #include "utils/jsonb.h" +#include "utils/jsonfuncs.h" typedef struct { @@ -184,6 +186,7 @@ extern bool jspGetBool(JsonPathItem *v); extern char *jspGetString(JsonPathItem *v, int32 *len); extern bool jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, int i); +extern bool jspIsMutable(JsonPath *path, List *varnames, List *varexprs); extern const char *jspOperationName(JsonPathItemType type); @@ -261,4 +264,28 @@ extern bool jspConvertRegexFlags(uint32 xflags, int *result, struct Node *escontext); +/* + * Evaluation of jsonpath + */ + +/* External variable passed into jsonpath. */ +typedef struct JsonPathVariable +{ + char *name; + Oid typid; + int32 typmod; + Datum value; + bool isnull; +} JsonPathVariable; + +/* SQL/JSON item */ +extern void JsonItemFromDatum(Datum val, Oid typid, int32 typmod, + JsonbValue *res); + +extern bool JsonPathExists(Datum jb, JsonPath *path, List *vars, bool *error); +extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, + bool *empty, bool *error, List *vars); +extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, + bool *error, List *vars); + #endif diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer index 435c139ec2..b2aa44f36d 100644 --- a/src/interfaces/ecpg/preproc/ecpg.trailer +++ b/src/interfaces/ecpg/preproc/ecpg.trailer @@ -651,6 +651,34 @@ var_type: simple_type $$.type_index = mm_strdup("-1"); $$.type_sizeof = NULL; } + | STRING_P + { + if (INFORMIX_MODE) + { + /* In Informix mode, "string" is automatically a typedef */ + $$.type_enum = ECPGt_string; + $$.type_str = mm_strdup("char"); + $$.type_dimension = mm_strdup("-1"); + $$.type_index = mm_strdup("-1"); + $$.type_sizeof = NULL; + } + else + { + /* Otherwise, legal only if user typedef'ed it */ + struct typedefs *this = get_typedef("string", false); + + $$.type_str = (this->type->type_enum == ECPGt_varchar || this->type->type_enum == ECPGt_bytea) ? EMPTY : mm_strdup(this->name); + $$.type_enum = this->type->type_enum; + $$.type_dimension = this->type->type_dimension; + $$.type_index = this->type->type_index; + if (this->type->type_sizeof && strlen(this->type->type_sizeof) != 0) + $$.type_sizeof = this->type->type_sizeof; + else + $$.type_sizeof = cat_str(3, mm_strdup("sizeof("), mm_strdup(this->name), mm_strdup(")")); + + struct_member_list[struct_level] = ECPGstruct_member_dup(this->struct_member_list); + } + } | INTERVAL ecpg_interval { $$.type_enum = ECPGt_interval; diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out new file mode 100644 index 0000000000..8b87580752 --- /dev/null +++ b/src/test/regress/expected/json_sqljson.out @@ -0,0 +1,18 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); +ERROR: JSON_EXISTS() is not yet implemented for the json type +LINE 1: SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_VALUE +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); +ERROR: JSON_VALUE() is not yet implemented for the json type +LINE 1: SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_QUERY +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); +ERROR: JSON_QUERY() is not yet implemented for the json type +LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out new file mode 100644 index 0000000000..24a1e3eabf --- /dev/null +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -0,0 +1,1020 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL::jsonb, '$'); + json_exists +------------- + +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb 'null', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_EXISTS(jsonb 'null', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{}', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + json_exists +------------- + f +(1 row) + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + json_exists +------------- + t +(1 row) + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); + json_exists +------------- + 1 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); + json_exists +------------- + 0 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); + json_exists +------------- + true +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +ERROR: cannot cast type boolean to jsonb +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +ERROR: cannot cast type boolean to real +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); + ^ +-- JSON_VALUE +SELECT JSON_VALUE(NULL::jsonb, '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$'); + json_value +------------ + true +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + json_value +------------ + t +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); + json_value +------------ + 123 +(1 row) + +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); +ERROR: SQL/JSON item cannot be cast to target type +SELECT JSON_VALUE(jsonb '1.23', '$'); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "1.23" +SELECT JSON_VALUE(jsonb '"aaa"', '$'); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); + json_value +------------ + aa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "aaa" +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); + json_value +------------ + 111 +(1 row) + +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + ?column? +------------ + 03-01-2017 +(1 row) + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '[]', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '{}', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '1', '$.a'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); + json_value +------------ + error +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 3 +(1 row) + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); + json_value +------------ + 0 +(1 row) + +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: " " +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 5 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 1 +(1 row) + +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + x | y +---+---- + 0 | -2 + 1 | 2 + 2 | -1 +(3 rows) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); + json_value +------------ + (1,2) +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + json_value +------------ + (1,2) +(1 row) + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); + json_value +-------------------------- + Tue Feb 20 18:34:56 2018 +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- JSON_QUERY +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + json_query | json_query | json_query | json_query | json_query +--------------------+--------------------+--------------------+----------------------+---------------------- + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, null, "2"] | [1, null, "2"] | [1, null, "2"] | [[1, null, "2"]] | [[1, null, "2"]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] +(6 rows) + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + unspec | without | with cond | with uncond | with +--------------------+--------------------+---------------------+----------------------+---------------------- + | | | | + | | | | + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, 2, 3] | [1, 2, 3] | [1, 2, 3] | [[1, 2, 3]] | [[1, 2, 3]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] + | | [1, "2", null, [3]] | [1, "2", null, [3]] | [1, "2", null, [3]] +(9 rows) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + json_query +------------ + \x616161 +(1 row) + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTE... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTE... + ^ +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_QUERY should return singleton item without wrapper +HINT: Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array. +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); + json_query +------------ + [1, +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); +ERROR: expected JSON array +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + x | y | list +---+---+-------------- + 0 | 0 | [] + 0 | 1 | [1] + 0 | 2 | [1, 2] + 0 | 3 | [1, 2, 3] + 0 | 4 | [1, 2, 3, 4] + 1 | 0 | [] + 1 | 1 | [1] + 1 | 2 | [1, 2] + 1 | 3 | [1, 2, 3] + 1 | 4 | [1, 2, 3, 4] + 2 | 0 | [] + 2 | 1 | [] + 2 | 2 | [2] + 2 | 3 | [2, 3] + 2 | 4 | [2, 3, 4] + 3 | 0 | [] + 3 | 1 | [] + 3 | 2 | [] + 3 | 3 | [3] + 3 | 4 | [3, 4] + 4 | 0 | [] + 4 | 1 | [] + 4 | 2 | [] + 4 | 3 | [] + 4 | 4 | [4] +(25 rows) + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); + json_query +----------------------------------------------------- + (1,aaa,"[1, ""2"", {}]","{""x"": [1, ""2"", {}]}",) +(1 row) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); + unnest +------------------------ + {"a": 1, "b": ["foo"]} + {"a": 2, "c": {}} + 123 +(3 rows) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); + json_query +-------------- + {1,2,NULL,3} +(1 row) + +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); + json_query +------------ + 1 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- Test constraints +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); +\d test_jsonb_constraints + Table "public.test_jsonb_constraints" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+-------------------------------------------------------------------------------- + js | text | | | + i | integer | | | + x | jsonb | | | JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +Check constraints: + "test_jsonb_constraint1" CHECK (js IS JSON) + "test_jsonb_constraint2" CHECK (JSON_EXISTS(js::jsonb, '$."a"' PASSING i + 5 AS int, i::text AS txt, ARRAY[1, 2, 3] AS arr)) + "test_jsonb_constraint3" CHECK (JSON_VALUE(js::jsonb, '$."a"' RETURNING integer DEFAULT ('12'::text || i)::integer ON EMPTY ERROR ON ERROR) > i) + "test_jsonb_constraint4" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb) + "test_jsonb_constraint5" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C")) + "test_jsonb_constraint6" CHECK (JSON_EXISTS(js::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2) + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%'; + check_clause +-------------------------------------------------------------------------------------------------------------------------- + ((js IS JSON)) + (JSON_EXISTS((js)::jsonb, '$."a"' PASSING (i + 5) AS int, (i)::text AS txt, ARRAY[1, 2, 3] AS arr)) + ((JSON_VALUE((js)::jsonb, '$."a"' RETURNING integer DEFAULT (('12'::text || i))::integer ON EMPTY ERROR ON ERROR) > i)) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb)) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C"))) + ((JSON_EXISTS((js)::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2)) +(6 rows) + +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint1" +DETAIL: Failing row contains (, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains (1, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('[]'); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ([], null, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ({"b": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint3" +DETAIL: Failing row contains ({"a": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint5" +DETAIL: Failing row contains ({"a": 7}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint4" +DETAIL: Failing row contains ({"a": 10}, 1, [1, 2]). +DROP TABLE test_jsonb_constraints; +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index cf46fa3359..cb3230f4dd 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -103,7 +103,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo # ---------- # Another group of parallel tests (JSON related) # ---------- -test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson +test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson json_sqljson jsonb_sqljson # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql new file mode 100644 index 0000000000..4f30fa46b9 --- /dev/null +++ b/src/test/regress/sql/json_sqljson.sql @@ -0,0 +1,11 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + +-- JSON_QUERY + +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql new file mode 100644 index 0000000000..0c3a7cc597 --- /dev/null +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -0,0 +1,318 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL::jsonb, '$'); + +SELECT JSON_EXISTS(jsonb '[]', '$'); +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + +SELECT JSON_EXISTS(jsonb '1', '$'); +SELECT JSON_EXISTS(jsonb 'null', '$'); +SELECT JSON_EXISTS(jsonb '[]', '$'); + +SELECT JSON_EXISTS(jsonb '1', '$.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_EXISTS(jsonb 'null', '$.a'); +SELECT JSON_EXISTS(jsonb '[]', '$.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); +SELECT JSON_EXISTS(jsonb '{}', '$.a'); +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); + + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL::jsonb, '$'); + +SELECT JSON_VALUE(jsonb 'null', '$'); +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + +SELECT JSON_VALUE(jsonb 'true', '$'); +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + +SELECT JSON_VALUE(jsonb '123', '$'); +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1.23', '$'); +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '"aaa"', '$'); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); + +SELECT JSON_VALUE(jsonb '[]', '$'); +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '{}', '$'); +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1', '$.a'); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- JSON_QUERY + +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); + +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); + +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); + +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- Test constraints + +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); + +\d test_jsonb_constraints + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%'; + +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +INSERT INTO test_jsonb_constraints VALUES ('[]'); +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); + +DROP TABLE test_jsonb_constraints; + +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 260854747b..3ce557468a 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1254,14 +1254,24 @@ JsonArrayAgg JsonArrayConstructor JsonArrayQueryConstructor JsonBaseObjectInfo +JsonBehavior +JsonBehaviorType +JsonCoercion +JsonCoercionState +JsonCommon JsonConstructorExpr JsonConstructorExprState JsonConstructorType JsonEncoding +JsonExpr +JsonExprOp +JsonExprState JsonFormat JsonFormatType JsonHashEntry JsonIsPredicate +JsonItemCoercions +JsonItemCoercionsState JsonIterateStringValuesAction JsonKeyValue JsonLexContext @@ -1294,6 +1304,10 @@ JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback JsonPathString +JsonPathVarCallback +JsonPathVariable +JsonPathVariableEvalContext +JsonQuotes JsonReturning JsonSemAction JsonTokenType -- 2.35.3
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-06-21 08:25  Amit Langote <amitlangote09@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 2 replies; 300+ messages in thread

From: Amit Langote @ 2023-06-21 08:25 UTC (permalink / raw)
  To: pgsql-hackers; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>

On Mon, Jun 19, 2023 at 5:31 PM Amit Langote <amitlangote09@gmail.com> wrote:
> So the following sql/json things still remain to be done:
>
> * sql/json query functions:
>     json_exists()
>     json_query()
>     json_value()
>
> * other sql/json functions:
>     json()
>     json_scalar()
>     json_serialize()
>
> * finally:
>     json_table
>
> Attached is the rebased patch for the 1st part.
...
> I'll rebase and post the patches for "other sql/json functions" and
> "json_table" shortly.

And here they are.

I realized that the patch for the "other sql/json functions" part is
relatively straightforward and has no dependence on the "sql/json
query functions" part getting done first.  So I've made that one the
0001 patch.  The patch I posted in the last email is now 0002, though
it only has changes related to changing the order of the patch, so I
decided not to change the patch version marker (v1).

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

Attachments: download all (4 files, .tar.gz)

v1-0001-SQL-JSON-functions.patch (application/octet-stream, 57.8K) download From a9a7a29007c39d79745264a2f2e977c8d53c489a Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:12:39 +0900 Subject: [PATCH v1 1/4] SQL JSON functions This Patch introduces three SQL standard JSON functions: JSON() JSON_SCALAR() JSON_SERIALIZE() JSON() produces json values from text, bytea, json or jsonb values, and has facilitites for handling duplicate keys. JSON_SCALAR() produces a json value from any scalar sql value, including json and jsonb. JSON_SERIALIZE() produces text or bytea from input which containis or represents json or jsonb; For the most part these functions don't add any significant new capabilities, but they will be of use to users wanting standard compliant JSON handling. Nikita Glukhov Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby. Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org --- doc/src/sgml/func.sgml | 71 ++++ doc/src/sgml/keywords/sql2016-02-reserved.txt | 3 + src/backend/executor/execExpr.c | 45 +++ src/backend/executor/execExprInterp.c | 42 +- src/backend/nodes/nodeFuncs.c | 30 ++ src/backend/parser/gram.y | 65 ++- src/backend/parser/parse_expr.c | 220 ++++++++-- src/backend/parser/parse_target.c | 9 + src/backend/utils/adt/format_type.c | 4 + src/backend/utils/adt/json.c | 44 +- src/backend/utils/adt/jsonb.c | 76 ++-- src/backend/utils/adt/ruleutils.c | 14 +- src/include/nodes/parsenodes.h | 48 +++ src/include/nodes/primnodes.h | 5 +- src/include/parser/kwlist.h | 4 +- src/include/utils/json.h | 19 + src/include/utils/jsonb.h | 21 + src/test/regress/expected/sqljson.out | 376 ++++++++++++++++++ src/test/regress/sql/sqljson.sql | 93 +++++ src/tools/pgindent/typedefs.list | 1 + 20 files changed, 1101 insertions(+), 89 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 5a47ce4343..8de8f527a5 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16001,6 +16001,77 @@ table2-mapping <returnvalue>{"a": "1", "b": "2"}</returnvalue> </para></entry> </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json constructor</primary></indexterm> + <function>json</function> ( + <replaceable>expression</replaceable> + <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional></optional> + <optional> { <literal>WITH</literal> | <literal>WITHOUT</literal> } <literal>UNIQUE</literal> <optional> <literal>KEYS</literal> </optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>json_data_type</replaceable> </optional>) + </para> + <para> + The <replaceable>expression</replaceable> can be any text type or a + <type>bytea</type> in UTF8 encoding. If the + <replaceable>expression</replaceable> is NULL, an + <acronym>SQL</acronym> null value is returned. + If <literal>WITH UNIQUE</literal> is specified, the + <replaceable>expression</replaceable> must not contain any duplicate + object keys. + </para> + <para> + <literal>json('{"a":123, "b":[true,"foo"], "a":"bar"}')</literal> + <returnvalue>{"a":123, "b":[true,"foo"], "a":"bar"}</returnvalue> + </para> + <para> + <literal>json('{"a":123,"b":[true,"foo"],"a":"bar"}' returning jsonb)</literal> + <returnvalue>{"a": "bar", "b": [true, "foo"]}</returnvalue> + </para> + </entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json_scalar</primary></indexterm> + <function>json_scalar</function> (<replaceable>expression</replaceable>) + <optional> <literal>RETURNING</literal> <replaceable>json_data_type</replaceable> </optional>) + </para> + <para> + Returns a JSON scalar value representing + <replaceable>expression</replaceable>. + If the input is NULL, an SQL NULL is returned. If the input is a number + or a boolean value, a corresponding JSON number or boolean value is + returned. For any other value a JSON string is returned. + </para> + <para> + <literal>json_scalar(123.45)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_scalar(CURRENT_TIMESTAMP)</literal> + <returnvalue>"2022-05-10T10:51:04.62128-04:00"</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <function>json_serialize</function> ( + <replaceable>expression</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional>) + </para> + <para> + Transforms an SQL/JSON value into a character or binary string. The + <replaceable>expression</replaceable> can be of any JSON type, any + character string type, or <type>bytea</type> in UTF8 encoding. + The returned type can be any character string type or + <type>bytea</type>. The default is <type>text</type>. + </para> + <para> + <literal>json_serialize('{ "a" : 1 } ' RETURNING bytea)</literal> + <returnvalue>\x7b20226122203a2031207d20</returnvalue> + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/keywords/sql2016-02-reserved.txt b/doc/src/sgml/keywords/sql2016-02-reserved.txt index f65dd4d577..3ee9492024 100644 --- a/doc/src/sgml/keywords/sql2016-02-reserved.txt +++ b/doc/src/sgml/keywords/sql2016-02-reserved.txt @@ -157,12 +157,15 @@ INTERVAL INTO IS JOIN +JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG JSON_QUERY +JSON_SCALAR +JSON_SERIALIZE JSON_TABLE JSON_TABLE_PRIMITIVE JSON_VALUE diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index e6e616865c..b958ae5985 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -48,6 +48,8 @@ #include "utils/array.h" #include "utils/builtins.h" #include "utils/datum.h" +#include "utils/json.h" +#include "utils/jsonb.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -2324,6 +2326,12 @@ ExecInitExprRec(Expr *node, ExprState *state, { ExecInitExprRec(ctor->func, state, resv, resnull); } + else if ((ctor->type == JSCTOR_JSON_PARSE && !ctor->unique) || + ctor->type == JSCTOR_JSON_SERIALIZE) + { + /* Use the value of the first argument as a result */ + ExecInitExprRec(linitial(args), state, resv, resnull); + } else { JsonConstructorExprState *jcstate; @@ -2362,6 +2370,43 @@ ExecInitExprRec(Expr *node, ExprState *state, argno++; } + /* prepare type cache for datum_to_json[b]() */ + if (ctor->type == JSCTOR_JSON_SCALAR) + { + bool is_jsonb = + ctor->returning->format->format_type == JS_FORMAT_JSONB; + + jcstate->arg_type_cache = + palloc(sizeof(*jcstate->arg_type_cache) * nargs); + + for (int i = 0; i < nargs; i++) + { + int category; + Oid outfuncid; + Oid typid = jcstate->arg_types[i]; + + if (is_jsonb) + { + JsonbTypeCategory jbcat; + + jsonb_categorize_type(typid, &jbcat, &outfuncid); + + category = (int) jbcat; + } + else + { + JsonTypeCategory jscat; + + json_categorize_type(typid, &jscat, &outfuncid); + + category = (int) jscat; + } + + jcstate->arg_type_cache[i].outfuncid = outfuncid; + jcstate->arg_type_cache[i].category = category; + } + } + ExprEvalPushStep(state, &scratch); } diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 7a4d7a4eee..9a234eb8ba 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -3987,7 +3987,7 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_values, jcstate->arg_nulls, jcstate->arg_types, - jcstate->constructor->absent_on_null); + ctor->absent_on_null); else if (ctor->type == JSCTOR_JSON_OBJECT) res = (is_jsonb ? jsonb_build_object_worker : @@ -3997,6 +3997,46 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_types, jcstate->constructor->absent_on_null, jcstate->constructor->unique); + else if (ctor->type == JSCTOR_JSON_SCALAR) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + int category = jcstate->arg_type_cache[0].category; + Oid outfuncid = jcstate->arg_type_cache[0].outfuncid; + + if (is_jsonb) + res = to_jsonb_worker(value, category, outfuncid); + else + res = to_json_worker(value, category, outfuncid); + } + } + else if (ctor->type == JSCTOR_JSON_PARSE) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + text *js = DatumGetTextP(value); + + if (is_jsonb) + res = jsonb_from_text(js, true); + else + { + (void) json_validate(js, true, true); + res = value; + } + } + } else elog(ERROR, "invalid JsonConstructorExpr type %d", ctor->type); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 0ed8712a63..5e9c46005d 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -3901,6 +3901,36 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonParseExpr: + { + JsonParseExpr *jpe = (JsonParseExpr *) node; + + if (WALK(jpe->expr)) + return true; + if (WALK(jpe->output)) + return true; + } + break; + case T_JsonScalarExpr: + { + JsonScalarExpr *jse = (JsonScalarExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; + case T_JsonSerializeExpr: + { + JsonSerializeExpr *jse = (JsonSerializeExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; case T_JsonConstructorExpr: { JsonConstructorExpr *ctor = (JsonConstructorExpr *) node; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 39ab7eac0d..8f239de65c 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -566,7 +566,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <list> copy_options %type <typnam> Typename SimpleTypename ConstTypename - GenericType Numeric opt_float + GenericType Numeric opt_float JsonType Character ConstCharacter CharacterWithLength CharacterWithoutLength ConstDatetime ConstInterval @@ -648,6 +648,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> json_format_clause_opt json_value_expr + json_returning_clause_opt json_output_clause_opt json_name_and_value json_aggregate_func @@ -726,6 +727,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG + JSON_SCALAR JSON_SERIALIZE KEY KEYS @@ -13984,6 +13986,7 @@ SimpleTypename: $$->typmods = list_make2(makeIntConst(INTERVAL_FULL_RANGE, -1), makeIntConst($3, @3)); } + | JsonType { $$ = $1; } ; /* We have a separate ConstTypename to allow defaulting fixed-length @@ -14002,6 +14005,7 @@ ConstTypename: | ConstBit { $$ = $1; } | ConstCharacter { $$ = $1; } | ConstDatetime { $$ = $1; } + | JsonType { $$ = $1; } ; /* @@ -14370,6 +14374,13 @@ interval_second: } ; +JsonType: + JSON + { + $$ = SystemTypeName("json"); + $$->location = @1; + } + ; /***************************************************************************** * @@ -15628,7 +15639,37 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } - ; + | JSON '(' json_value_expr json_key_uniqueness_constraint_opt + json_returning_clause_opt ')' + { + JsonParseExpr *n = makeNode(JsonParseExpr); + + n->expr = (JsonValueExpr *) $3; + n->unique_keys = $4; + n->output = (JsonOutput *) $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SCALAR '(' a_expr json_returning_clause_opt ')' + { + JsonScalarExpr *n = makeNode(JsonScalarExpr); + + n->expr = (Expr *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SERIALIZE '(' json_value_expr json_output_clause_opt ')' + { + JsonSerializeExpr *n = makeNode(JsonSerializeExpr); + + n->expr = (JsonValueExpr *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + ; + /* * SQL/XML support @@ -16376,6 +16417,20 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; +json_returning_clause_opt: + RETURNING Typename + { + JsonOutput *n = makeNode(JsonOutput); + + n->typeName = $2; + n->returning = makeNode(JsonReturning); + n->returning->format = + makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, @2); + $$ = (Node *) n; + } + | /* EMPTY */ { $$ = NULL; } + ; + json_output_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17067,7 +17122,6 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION - | JSON | KEY | KEYS | LABEL @@ -17282,10 +17336,13 @@ col_name_keyword: | INT_P | INTEGER | INTERVAL + | JSON | JSON_ARRAY | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | LEAST | NATIONAL | NCHAR @@ -17646,6 +17703,8 @@ bare_label_keyword: | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | KEY | KEYS | LABEL diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 346fd272b6..3a44755515 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -86,6 +86,10 @@ static Node *transformJsonArrayQueryConstructor(ParseState *pstate, static Node *transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg); static Node *transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg); static Node *transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred); +static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); +static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); +static Node *transformJsonSerializeExpr(ParseState *pstate, + JsonSerializeExpr * expr); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -337,6 +341,18 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonIsPredicate(pstate, (JsonIsPredicate *) expr); break; + case T_JsonParseExpr: + result = transformJsonParseExpr(pstate, (JsonParseExpr *) expr); + break; + + case T_JsonScalarExpr: + result = transformJsonScalarExpr(pstate, (JsonScalarExpr *) expr); + break; + + case T_JsonSerializeExpr: + result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3219,11 +3235,14 @@ makeCaseTestExpr(Node *expr) /* * Transform JSON value expression using specified input JSON format or - * default format otherwise. + * default format otherwise, reconciling with the requeted output type if + * any. */ static Node * transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, - JsonFormatType default_format) + char *constructName, + JsonFormatType default_format, + Oid targettype) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3233,12 +3252,8 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, char typcategory; bool typispreferred; - /* - * Using JSON_VALUE here is slightly bogus: perhaps we need to be passed a - * JsonConstructorType so that we can use one of JSON_OBJECTAGG, etc. - */ if (exprType(expr) == UNKNOWNOID) - expr = coerce_to_specific_type(pstate, expr, TEXTOID, "JSON_VALUE"); + expr = coerce_to_specific_type(pstate, expr, TEXTOID, constructName); rawexpr = expr; exprtype = exprType(expr); @@ -3269,15 +3284,14 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, else format = default_format; - if (format != JS_FORMAT_DEFAULT) + if (format != JS_FORMAT_DEFAULT || + (OidIsValid(targettype) && exprtype != targettype)) { - Oid targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; - Node *orig = makeCaseTestExpr(expr); Node *coerced; + bool cast_is_needed = OidIsValid(targettype); - expr = orig; - - if (exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) + if (!cast_is_needed && + exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), errmsg(ve->format->format_type == JS_FORMAT_DEFAULT ? @@ -3293,6 +3307,9 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, exprtype = TEXTOID; } + if (!OidIsValid(targettype)) + targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; + /* Try to coerce to the target type. */ coerced = coerce_to_target_type(pstate, expr, exprtype, targettype, -1, @@ -3303,18 +3320,27 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, if (!coerced) { /* If coercion failed, use to_json()/to_jsonb() functions. */ - Oid fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; - FuncExpr *fexpr = makeFuncExpr(fnoid, targettype, - list_make1(expr), - InvalidOid, InvalidOid, - COERCE_EXPLICIT_CALL); + FuncExpr *fexpr; + Oid fnoid; + + if (cast_is_needed) /* only CAST is allowed */ + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(exprtype), + format_type_be(targettype)), + parser_errposition(pstate, location))); + + fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; + fexpr = makeFuncExpr(fnoid, targettype, list_make1(expr), + InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL); fexpr->location = location; coerced = (Node *) fexpr; } - if (coerced == orig) + if (coerced == expr) expr = rawexpr; else { @@ -3589,7 +3615,9 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) JsonKeyValue *kv = castNode(JsonKeyValue, lfirst(lc)); Node *key = transformExprRecurse(pstate, (Node *) kv->key); Node *val = transformJsonValueExpr(pstate, kv->value, - JS_FORMAT_DEFAULT); + "JSON_OBJECT()", + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, key); args = lappend(args, val); @@ -3768,7 +3796,8 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) Oid aggtype; key = transformExprRecurse(pstate, (Node *) agg->arg->key); - val = transformJsonValueExpr(pstate, agg->arg->value, JS_FORMAT_DEFAULT); + val = transformJsonValueExpr(pstate, agg->arg->value, "JSON_OBJECTAGG()", + JS_FORMAT_DEFAULT, InvalidOid); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3824,7 +3853,8 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) Oid aggfnoid; Oid aggtype; - arg = transformJsonValueExpr(pstate, agg->arg, JS_FORMAT_DEFAULT); + arg = transformJsonValueExpr(pstate, agg->arg, "JSON_ARRAYAGG()", + JS_FORMAT_DEFAULT, InvalidOid); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3871,7 +3901,9 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) { JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, jsval, - JS_FORMAT_DEFAULT); + "JSON_ARRAY()", + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, val); } @@ -3954,3 +3986,145 @@ transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred) return makeJsonIsPredicate(expr, NULL, pred->item_type, pred->unique_keys, pred->location); } + +/* + * Transform the output clause of a JSON_*() expression if there is one and + * create one of it not. + */ +static JsonReturning * +transformJsonReturning(ParseState *pstate, JsonOutput *output, const char *fname) +{ + JsonReturning *returning; + + if (output) + { + returning = transformJsonOutput(pstate, output, false); + + Assert(OidIsValid(returning->typid)); + + if (returning->typid != JSONOID && returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), fname), + parser_errposition(pstate, output->typeName->location))); + } + else + { + /* Output type is JSON by default. */ + Oid targettype = JSONOID; + JsonFormatType format = JS_FORMAT_JSON; + + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(format, JS_ENC_DEFAULT, -1); + returning->typid = targettype; + returning->typmod = -1; + } + + return returning; +} + +/* + * Transform a JSON() expression. + */ +static Node * +transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) +{ + JsonReturning *returning = transformJsonReturning(pstate, jsexpr->output, + "JSON()"); + Node *arg; + + if (jsexpr->unique_keys) + { + /* + * Coerce string argument to text and then to json[b] in the executor + * node with key uniqueness check. + */ + JsonValueExpr *jve = jsexpr->expr; + Oid arg_type; + + arg = transformJsonParseArg(pstate, (Node *) jve->raw_expr, jve->format, + &arg_type); + + if (arg_type != TEXTOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use non-string types with WITH UNIQUE KEYS clause"), + parser_errposition(pstate, jsexpr->location))); + } + else + { + /* + * Coerce argument to target type using CAST for compatibility with PG + * function-like CASTs. + */ + arg = transformJsonValueExpr(pstate, jsexpr->expr, "JSON()", + JS_FORMAT_JSON, returning->typid); + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, + returning, jsexpr->unique_keys, false, + jsexpr->location); +} + +/* + * Transform a JSON_SCALAR() expression. + */ +static Node * +transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *jsexpr) +{ + Node *arg = transformExprRecurse(pstate, (Node *) jsexpr->expr); + JsonReturning *returning = transformJsonReturning(pstate, jsexpr->output, + "JSON_SCALAR()"); + + if (exprType(arg) == UNKNOWNOID) + arg = coerce_to_specific_type(pstate, arg, TEXTOID, "JSON_SCALAR"); + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SCALAR, list_make1(arg), NULL, + returning, false, false, jsexpr->location); +} + +/* + * Transform a JSON_SERIALIZE() expression. + */ +static Node * +transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) +{ + Node *arg = transformJsonValueExpr(pstate, expr->expr, + "JSON_SERIALIZE()", + JS_FORMAT_JSON, + InvalidOid); + JsonReturning *returning; + + if (expr->output) + { + returning = transformJsonOutput(pstate, expr->output, true); + + if (returning->typid != BYTEAOID) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(returning->typid, &typcategory, + &typispreferred); + if (typcategory != TYPCATEGORY_STRING) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), + "JSON_SERIALIZE()"), + errhint("Try returning a string type or bytea."))); + } + } + else + { + /* RETURNING TEXT FORMAT JSON is by default */ + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(JS_FORMAT_JSON, JS_ENC_DEFAULT, -1); + returning->typid = TEXTOID; + returning->typmod = -1; + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), + NULL, returning, false, false, expr->location); +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4cca97ff9c..520d4f2a23 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1953,6 +1953,15 @@ FigureColnameInternal(Node *node, char **name) /* make XMLSERIALIZE act like a regular function */ *name = "xmlserialize"; return 2; + case T_JsonParseExpr: + *name = "json"; + return 2; + case T_JsonScalarExpr: + *name = "json_scalar"; + return 2; + case T_JsonSerializeExpr: + *name = "json_serialize"; + return 2; case T_JsonObjectConstructor: /* make JSON_OBJECT act like a regular function */ *name = "json_object"; diff --git a/src/backend/utils/adt/format_type.c b/src/backend/utils/adt/format_type.c index 12402a0637..36c45a3978 100644 --- a/src/backend/utils/adt/format_type.c +++ b/src/backend/utils/adt/format_type.c @@ -294,6 +294,10 @@ format_type_extended(Oid type_oid, int32 typemod, bits16 flags) else buf = pstrdup("character varying"); break; + + case JSONOID: + buf = pstrdup("json"); + break; } if (buf == NULL) diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index 49080e5fbf..6d8fcdf40d 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -29,21 +29,6 @@ #include "utils/lsyscache.h" #include "utils/typcache.h" -typedef enum /* type categories for datum_to_json */ -{ - JSONTYPE_NULL, /* null, so we didn't bother to identify */ - JSONTYPE_BOOL, /* boolean (built-in types only) */ - JSONTYPE_NUMERIC, /* numeric (ditto) */ - JSONTYPE_DATE, /* we use special formatting for datetimes */ - JSONTYPE_TIMESTAMP, - JSONTYPE_TIMESTAMPTZ, - JSONTYPE_JSON, /* JSON itself (and JSONB) */ - JSONTYPE_ARRAY, /* array */ - JSONTYPE_COMPOSITE, /* composite */ - JSONTYPE_CAST, /* something with an explicit cast to JSON */ - JSONTYPE_OTHER /* all else */ -} JsonTypeCategory; - /* * Support for fast key uniqueness checking. @@ -107,9 +92,6 @@ static void array_dim_to_json(StringInfo result, int dim, int ndims, int *dims, bool use_line_feeds); static void array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds); -static void json_categorize_type(Oid typoid, - JsonTypeCategory *tcategory, - Oid *outfuncoid); static void datum_to_json(Datum val, bool is_null, StringInfo result, JsonTypeCategory tcategory, Oid outfuncoid, bool key_scalar); @@ -188,8 +170,11 @@ json_recv(PG_FUNCTION_ARGS) * Given the datatype OID, return its JsonTypeCategory, as well as the type's * output function OID. If the returned category is JSONTYPE_CAST, we * return the OID of the type->JSON cast function instead. + * + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. */ -static void +void json_categorize_type(Oid typoid, JsonTypeCategory *tcategory, Oid *outfuncoid) @@ -771,6 +756,20 @@ row_to_json_pretty(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); } +/* + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. + */ +Datum +to_json_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid) +{ + StringInfo result = makeStringInfo(); + + datum_to_json(val, false, result, tcategory, outfuncoid, false); + + return PointerGetDatum(cstring_to_text_with_len(result->data, result->len)); +} + /* * Is the given type immutable when coming out of a JSON context? * @@ -821,7 +820,6 @@ to_json(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - StringInfo result; JsonTypeCategory tcategory; Oid outfuncoid; @@ -833,11 +831,7 @@ to_json(PG_FUNCTION_ARGS) json_categorize_type(val_type, &tcategory, &outfuncoid); - result = makeStringInfo(); - - datum_to_json(val, false, result, tcategory, outfuncoid, false); - - PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); + PG_RETURN_DATUM(to_json_worker(val, tcategory, outfuncoid)); } /* diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index cf43c3f2de..7a91177a55 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -34,26 +34,10 @@ typedef struct JsonbInState { JsonbParseState *parseState; JsonbValue *res; + bool unique_keys; Node *escontext; } JsonbInState; -/* unlike with json categories, we need to treat json and jsonb differently */ -typedef enum /* type categories for datum_to_jsonb */ -{ - JSONBTYPE_NULL, /* null, so we didn't bother to identify */ - JSONBTYPE_BOOL, /* boolean (built-in types only) */ - JSONBTYPE_NUMERIC, /* numeric (ditto) */ - JSONBTYPE_DATE, /* we use special formatting for datetimes */ - JSONBTYPE_TIMESTAMP, /* we use special formatting for timestamp */ - JSONBTYPE_TIMESTAMPTZ, /* ... and timestamptz */ - JSONBTYPE_JSON, /* JSON */ - JSONBTYPE_JSONB, /* JSONB */ - JSONBTYPE_ARRAY, /* array */ - JSONBTYPE_COMPOSITE, /* composite */ - JSONBTYPE_JSONCAST, /* something with an explicit cast to JSON */ - JSONBTYPE_OTHER /* all else */ -} JsonbTypeCategory; - typedef struct JsonbAggState { JsonbInState *res; @@ -63,7 +47,8 @@ typedef struct JsonbAggState Oid val_output_func; } JsonbAggState; -static inline Datum jsonb_from_cstring(char *json, int len, Node *escontext); +static inline Datum jsonb_from_cstring(char *json, int len, bool unique_keys, + Node *escontext); static bool checkStringLen(size_t len, Node *escontext); static JsonParseErrorType jsonb_in_object_start(void *pstate); static JsonParseErrorType jsonb_in_object_end(void *pstate); @@ -72,17 +57,11 @@ static JsonParseErrorType jsonb_in_array_end(void *pstate); static JsonParseErrorType jsonb_in_object_field_start(void *pstate, char *fname, bool isnull); static void jsonb_put_escaped_value(StringInfo out, JsonbValue *scalarVal); static JsonParseErrorType jsonb_in_scalar(void *pstate, char *token, JsonTokenType tokentype); -static void jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid); static void composite_to_jsonb(Datum composite, JsonbInState *result); static void array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *dims, Datum *vals, bool *nulls, int *valcount, JsonbTypeCategory tcategory, Oid outfuncoid); static void array_to_jsonb_internal(Datum array, JsonbInState *result); -static void jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid); static void datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, JsonbTypeCategory tcategory, Oid outfuncoid, bool key_scalar); @@ -100,7 +79,7 @@ jsonb_in(PG_FUNCTION_ARGS) { char *json = PG_GETARG_CSTRING(0); - return jsonb_from_cstring(json, strlen(json), fcinfo->context); + return jsonb_from_cstring(json, strlen(json), false, fcinfo->context); } /* @@ -124,7 +103,7 @@ jsonb_recv(PG_FUNCTION_ARGS) else elog(ERROR, "unsupported jsonb version number %d", version); - return jsonb_from_cstring(str, nbytes, NULL); + return jsonb_from_cstring(str, nbytes, false, NULL); } /* @@ -165,6 +144,18 @@ jsonb_send(PG_FUNCTION_ARGS) PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); } +/* + * Note: exported for use in SQL/JSON executor functions. + */ +Datum +jsonb_from_text(text *js, bool unique_keys) +{ + return jsonb_from_cstring(VARDATA_ANY(js), + VARSIZE_ANY_EXHDR(js), + unique_keys, + NULL); +} + /* * Get the type name of a jsonb container. */ @@ -258,7 +249,7 @@ jsonb_typeof(PG_FUNCTION_ARGS) * instead of being thrown. */ static inline Datum -jsonb_from_cstring(char *json, int len, Node *escontext) +jsonb_from_cstring(char *json, int len, bool unique_keys, Node *escontext) { JsonLexContext *lex; JsonbInState state; @@ -268,6 +259,7 @@ jsonb_from_cstring(char *json, int len, Node *escontext) memset(&sem, 0, sizeof(sem)); lex = makeJsonLexContextCstringLen(json, len, GetDatabaseEncoding(), true); + state.unique_keys = unique_keys; state.escontext = escontext; sem.semstate = (void *) &state; @@ -304,6 +296,7 @@ jsonb_in_object_start(void *pstate) JsonbInState *_state = (JsonbInState *) pstate; _state->res = pushJsonbValue(&_state->parseState, WJB_BEGIN_OBJECT, NULL); + _state->parseState->unique_keys = _state->unique_keys; return JSON_SUCCESS; } @@ -639,8 +632,11 @@ add_indent(StringInfo out, bool indent, int level) * Given the datatype OID, return its JsonbTypeCategory, as well as the type's * output function OID. If the returned category is JSONBTYPE_JSONCAST, * we return the OID of the relevant cast function instead. + * + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. */ -static void +void jsonb_categorize_type(Oid typoid, JsonbTypeCategory *tcategory, Oid *outfuncoid) @@ -1150,6 +1146,23 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, datum_to_jsonb(val, is_null, result, tcategory, outfuncoid, key_scalar); } + +/* + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. + */ +Datum +to_jsonb_worker(Datum val, JsonbTypeCategory tcategory, Oid outfuncoid) +{ + JsonbInState result; + + memset(&result, 0, sizeof(JsonbInState)); + + datum_to_jsonb(val, false, &result, tcategory, outfuncoid, false); + + return JsonbPGetDatum(JsonbValueToJsonb(result.res)); +} + /* * Is the given type immutable when coming out of a JSONB context? * @@ -1201,7 +1214,6 @@ to_jsonb(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - JsonbInState result; JsonbTypeCategory tcategory; Oid outfuncoid; @@ -1213,11 +1225,7 @@ to_jsonb(PG_FUNCTION_ARGS) jsonb_categorize_type(val_type, &tcategory, &outfuncoid); - memset(&result, 0, sizeof(JsonbInState)); - - datum_to_jsonb(val, false, &result, tcategory, outfuncoid, false); - - PG_RETURN_POINTER(JsonbValueToJsonb(result.res)); + PG_RETURN_DATUM(to_jsonb_worker(val, tcategory, outfuncoid)); } Datum diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d3a973d86b..d1b03d6cb2 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -10832,6 +10832,15 @@ get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, case JSCTOR_JSON_ARRAY: funcname = "JSON_ARRAY"; break; + case JSCTOR_JSON_PARSE: + funcname = "JSON"; + break; + case JSCTOR_JSON_SCALAR: + funcname = "JSON_SCALAR"; + break; + case JSCTOR_JSON_SERIALIZE: + funcname = "JSON_SERIALIZE"; + break; default: elog(ERROR, "invalid JsonConstructorType %d", ctor->type); } @@ -10879,7 +10888,10 @@ get_json_constructor_options(JsonConstructorExpr *ctor, StringInfo buf) if (ctor->unique) appendStringInfoString(buf, " WITH UNIQUE KEYS"); - get_json_returning(ctor->returning, buf, true); + if (!((ctor->type == JSCTOR_JSON_PARSE || + ctor->type == JSCTOR_JSON_SCALAR) && + ctor->returning->typid == JSONOID)) + get_json_returning(ctor->returning, buf, true); } /* diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index b3bec90e52..a495a814fd 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1715,6 +1715,17 @@ typedef struct TriggerTransition /* Nodes for SQL/JSON support */ +/* + * JsonQuotes - + * representation of [KEEP|OMIT] QUOTES clause for JSON_QUERY() + */ +typedef enum JsonQuotes +{ + JS_QUOTES_UNSPEC, /* unspecified */ + JS_QUOTES_KEEP, /* KEEP QUOTES */ + JS_QUOTES_OMIT /* OMIT QUOTES */ +} JsonQuotes; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1738,6 +1749,43 @@ typedef struct JsonKeyValue JsonValueExpr *value; /* JSON value expression */ } JsonKeyValue; +/* + * JsonParseExpr - + * untransformed representation of JSON() + */ +typedef struct JsonParseExpr +{ + NodeTag type; + JsonValueExpr *expr; /* string expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + bool unique_keys; /* WITH UNIQUE KEYS? */ + int location; /* token location, or -1 if unknown */ +} JsonParseExpr; + +/* + * JsonScalarExpr - + * untransformed representation of JSON_SCALAR() + */ +typedef struct JsonScalarExpr +{ + NodeTag type; + Expr *expr; /* scalar expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonScalarExpr; + +/* + * JsonSerializeExpr - + * untransformed representation of JSON_SERIALIZE() function + */ +typedef struct JsonSerializeExpr +{ + NodeTag type; + JsonValueExpr *expr; /* json value expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonSerializeExpr; + /* * JsonObjectConstructor - * untransformed representation of JSON_OBJECT() constructor diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 792a743f72..6c6a1810af 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1607,7 +1607,10 @@ typedef enum JsonConstructorType JSCTOR_JSON_OBJECT = 1, JSCTOR_JSON_ARRAY = 2, JSCTOR_JSON_OBJECTAGG = 3, - JSCTOR_JSON_ARRAYAGG = 4 + JSCTOR_JSON_ARRAYAGG = 4, + JSCTOR_JSON_SCALAR = 5, + JSCTOR_JSON_SERIALIZE = 6, + JSCTOR_JSON_PARSE = 7 } JsonConstructorType; /* diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index f5b2e61ca5..5984dcfa4b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -230,11 +230,13 @@ PG_KEYWORD("is", IS, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("isnull", ISNULL, TYPE_FUNC_NAME_KEYWORD, AS_LABEL) PG_KEYWORD("isolation", ISOLATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) -PG_KEYWORD("json", JSON, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/json.h b/src/include/utils/json.h index 35a9a5545d..4c0e0bd09d 100644 --- a/src/include/utils/json.h +++ b/src/include/utils/json.h @@ -16,11 +16,30 @@ #include "lib/stringinfo.h" +typedef enum /* type categories for datum_to_json */ +{ + JSONTYPE_NULL, /* null, so we didn't bother to identify */ + JSONTYPE_BOOL, /* boolean (built-in types only) */ + JSONTYPE_NUMERIC, /* numeric (ditto) */ + JSONTYPE_DATE, /* we use special formatting for datetimes */ + JSONTYPE_TIMESTAMP, + JSONTYPE_TIMESTAMPTZ, + JSONTYPE_JSON, /* JSON itself (and JSONB) */ + JSONTYPE_ARRAY, /* array */ + JSONTYPE_COMPOSITE, /* composite */ + JSONTYPE_CAST, /* something with an explicit cast to JSON */ + JSONTYPE_OTHER /* all else */ +} JsonTypeCategory; + /* functions in json.c */ extern void escape_json(StringInfo buf, const char *str); extern char *JsonEncodeDateTime(char *buf, Datum value, Oid typid, const int *tzp); extern bool to_json_is_immutable(Oid typoid); +extern void json_categorize_type(Oid typoid, JsonTypeCategory *tcategory, + Oid *outfuncoid); +extern Datum to_json_worker(Datum val, JsonTypeCategory tcategory, + Oid outfuncoid); extern Datum json_build_object_worker(int nargs, Datum *args, bool *nulls, Oid *types, bool absent_on_null, bool unique_keys); diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index 649a1644f2..8417a74298 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -368,6 +368,22 @@ typedef struct JsonbIterator struct JsonbIterator *parent; } JsonbIterator; +/* unlike with json categories, we need to treat json and jsonb differently */ +typedef enum /* type categories for datum_to_jsonb */ +{ + JSONBTYPE_NULL, /* null, so we didn't bother to identify */ + JSONBTYPE_BOOL, /* boolean (built-in types only) */ + JSONBTYPE_NUMERIC, /* numeric (ditto) */ + JSONBTYPE_DATE, /* we use special formatting for datetimes */ + JSONBTYPE_TIMESTAMP, /* we use special formatting for timestamp */ + JSONBTYPE_TIMESTAMPTZ, /* ... and timestamptz */ + JSONBTYPE_JSON, /* JSON */ + JSONBTYPE_JSONB, /* JSONB */ + JSONBTYPE_ARRAY, /* array */ + JSONBTYPE_COMPOSITE, /* composite */ + JSONBTYPE_JSONCAST, /* something with an explicit cast to JSON */ + JSONBTYPE_OTHER /* all else */ +} JsonbTypeCategory; /* Convenience macros */ static inline Jsonb * @@ -418,6 +434,7 @@ extern void JsonbHashScalarValueExtended(const JsonbValue *scalarVal, uint64 *hash, uint64 seed); /* jsonb.c support functions */ +extern Datum jsonb_from_text(text *js, bool unique_keys); extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, @@ -430,6 +447,10 @@ extern Datum jsonb_set_element(Jsonb *jb, Datum *path, int path_len, extern Datum jsonb_get_element(Jsonb *jb, Datum *path, int npath, bool *isnull, bool as_text); extern bool to_jsonb_is_immutable(Oid typoid); +extern void jsonb_categorize_type(Oid typoid, JsonbTypeCategory *tcategory, + Oid *outfuncoid); +extern Datum to_jsonb_worker(Datum val, JsonbTypeCategory tcategory, + Oid outfuncoid); extern Datum jsonb_build_object_worker(int nargs, Datum *args, bool *nulls, Oid *types, bool absent_on_null, bool unique_keys); diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index d73c7e2c6c..f0f5ae8119 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -1,3 +1,337 @@ +-- JSON() +SELECT JSON(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON(); + ^ +SELECT JSON(NULL); + json +------ + +(1 row) + +SELECT JSON('{ "a" : 1 } '); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +ERROR: JSON ENCODING clause is only allowed for bytea input type +LINE 1: SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); + ^ +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + pg_typeof +----------- + json +(1 row) + +SELECT JSON(' 1 '::json); + json +--------- + 1 +(1 row) + +SELECT JSON(' 1 '::jsonb); + json +------ + 1 +(1 row) + +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +ERROR: cannot use non-string types with WITH UNIQUE KEYS clause +LINE 1: SELECT JSON(' 1 '::json WITH UNIQUE KEYS); + ^ +SELECT JSON(123); +ERROR: cannot cast type integer to json +LINE 1: SELECT JSON(123); + ^ +SELECT JSON('{"a": 1, "a": 2}'); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +ERROR: duplicate JSON object key value +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); + QUERY PLAN +----------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); + QUERY PLAN +------------------------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON ENCODING UTF8) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); + QUERY PLAN +---------------------------------------------- + Result + Output: JSON('123'::text WITH UNIQUE KEYS) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +SELECT JSON('123' RETURNING text); +ERROR: cannot use RETURNING type text in JSON() +LINE 1: SELECT JSON('123' RETURNING text); + ^ +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING json); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING jsonb); + QUERY PLAN +---------------------------------------------- + Result + Output: JSON('123'::jsonb RETURNING jsonb) +(2 rows) + +SELECT pg_typeof(JSON('123')); + pg_typeof +----------- + json +(1 row) + +SELECT pg_typeof(JSON('123' RETURNING json)); + pg_typeof +----------- + json +(1 row) + +SELECT pg_typeof(JSON('123' RETURNING jsonb)); + pg_typeof +----------- + jsonb +(1 row) + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SCALAR(); + ^ +SELECT JSON_SCALAR(NULL); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(NULL::int); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(123); + json_scalar +------------- + 123 +(1 row) + +SELECT JSON_SCALAR(123.45); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(123.45::numeric); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(true); + json_scalar +------------- + true +(1 row) + +SELECT JSON_SCALAR(false); + json_scalar +------------- + false +(1 row) + +SELECT JSON_SCALAR(' 123.45'); + json_scalar +------------- + " 123.45" +(1 row) + +SELECT JSON_SCALAR('2020-06-07'::date); + json_scalar +-------------- + "2020-06-07" +(1 row) + +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); + json_scalar +----------------------- + "2020-06-07T01:02:03" +(1 row) + +SELECT JSON_SCALAR('{}'::json); + json_scalar +------------- + {} +(1 row) + +SELECT JSON_SCALAR('{}'::jsonb); + json_scalar +------------- + {} +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); + QUERY PLAN +---------------------------- + Result + Output: JSON_SCALAR(123) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); + QUERY PLAN +------------------------------------ + Result + Output: JSON_SCALAR('123'::text) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING json); + QUERY PLAN +---------------------------- + Result + Output: JSON_SCALAR(123) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING jsonb); + QUERY PLAN +-------------------------------------------- + Result + Output: JSON_SCALAR(123 RETURNING jsonb) +(2 rows) + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SERIALIZE(); + ^ +SELECT JSON_SERIALIZE(NULL); + json_serialize +---------------- + +(1 row) + +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } '); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('1'); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('1' FORMAT JSON); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); + json_serialize +---------------------------- + \x7b20226122203a2031207d20 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + pg_typeof +----------- + text +(1 row) + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); +ERROR: cannot use RETURNING type jsonb in JSON_SERIALIZE() +HINT: Try returning a string type or bytea. +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); + QUERY PLAN +----------------------------------------------------- + Result + Output: JSON_SERIALIZE('{}'::json RETURNING text) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + QUERY PLAN +------------------------------------------------------ + Result + Output: JSON_SERIALIZE('{}'::json RETURNING bytea) +(2 rows) + -- JSON_OBJECT() SELECT JSON_OBJECT(); json_object @@ -630,6 +964,13 @@ ERROR: duplicate JSON object key SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); ERROR: duplicate JSON object key +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + json_objectagg +------------------ + {"1": 1, "2": 2} +(1 row) + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -645,6 +986,41 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); CREATE OR REPLACE VIEW public.json_object_view AS SELECT JSON_OBJECT('foo' : '1'::text FORMAT JSON, 'bar' : 'baz'::text RETURNING json) AS "json_object" DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + a | json_objectagg +---------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(2 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":1,"v":null} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1, "2" : 2 } + {"k":1,"v":null} | { "1" : 1, "2" : 2 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 4fd820fd51..e338ef706b 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -1,3 +1,75 @@ +-- JSON() +SELECT JSON(); +SELECT JSON(NULL); +SELECT JSON('{ "a" : 1 } '); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + +SELECT JSON(' 1 '::json); +SELECT JSON(' 1 '::jsonb); +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +SELECT JSON(123); + +SELECT JSON('{"a": 1, "a": 2}'); +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + +SELECT JSON('123' RETURNING text); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING json); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING jsonb); +SELECT pg_typeof(JSON('123')); +SELECT pg_typeof(JSON('123' RETURNING json)); +SELECT pg_typeof(JSON('123' RETURNING jsonb)); + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +SELECT JSON_SCALAR(NULL); +SELECT JSON_SCALAR(NULL::int); +SELECT JSON_SCALAR(123); +SELECT JSON_SCALAR(123.45); +SELECT JSON_SCALAR(123.45::numeric); +SELECT JSON_SCALAR(true); +SELECT JSON_SCALAR(false); +SELECT JSON_SCALAR(' 123.45'); +SELECT JSON_SCALAR('2020-06-07'::date); +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); +SELECT JSON_SCALAR('{}'::json); +SELECT JSON_SCALAR('{}'::jsonb); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING json); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING jsonb); + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +SELECT JSON_SERIALIZE(NULL); +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); +SELECT JSON_SERIALIZE('{ "a" : 1 } '); +SELECT JSON_SERIALIZE('1'); +SELECT JSON_SERIALIZE('1' FORMAT JSON); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); + + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + -- JSON_OBJECT() SELECT JSON_OBJECT(); SELECT JSON_OBJECT(RETURNING json); @@ -216,6 +288,9 @@ FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -227,6 +302,24 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 260854747b..2f1e8a5702 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1296,6 +1296,7 @@ JsonPathPredicateCallback JsonPathString JsonReturning JsonSemAction +JsonSerializeExpr JsonTokenType JsonTransformStringValuesAction JsonTypeCategory -- 2.35.3
v1-0004-Claim-SQL-standard-compliance-for-SQL-JSON-featur.patch (application/octet-stream, 2.3K) download From 47ba9c09119a9edc0b8ee2416899bedec5d4acf1 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:16:39 +0900 Subject: [PATCH v1 4/4] Claim SQL standard compliance for SQL/JSON features Discussion: https://postgr.es/m/d03d809c-d0fb-fd6a-1476-d6dc18ec940e@dunslane.net --- src/backend/catalog/sql_features.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt index b33065d7bf..b379c71df9 100644 --- a/src/backend/catalog/sql_features.txt +++ b/src/backend/catalog/sql_features.txt @@ -548,15 +548,15 @@ T811 Basic SQL/JSON constructor functions YES T812 SQL/JSON: JSON_OBJECTAGG YES T813 SQL/JSON: JSON_ARRAYAGG with ORDER BY YES T814 Colon in JSON_OBJECT or JSON_OBJECTAGG YES -T821 Basic SQL/JSON query operators NO +T821 Basic SQL/JSON query operators YES T822 SQL/JSON: IS JSON WITH UNIQUE KEYS predicate YES -T823 SQL/JSON: PASSING clause NO -T824 JSON_TABLE: specific PLAN clause NO -T825 SQL/JSON: ON EMPTY and ON ERROR clauses NO -T826 General value expression in ON ERROR or ON EMPTY clauses NO -T827 JSON_TABLE: sibling NESTED COLUMNS clauses NO -T828 JSON_QUERY NO -T829 JSON_QUERY: array wrapper options NO +T823 SQL/JSON: PASSING clause YES +T824 JSON_TABLE: specific PLAN clause YES +T825 SQL/JSON: ON EMPTY and ON ERROR clauses YES +T826 General value expression in ON ERROR or ON EMPTY clauses YES +T827 JSON_TABLE: sibling NESTED COLUMNS clauses YES +T828 JSON_QUERY YES +T829 JSON_QUERY: array wrapper options YES T830 Enforcing unique keys in SQL/JSON constructor functions YES T831 SQL/JSON path language: strict mode YES T832 SQL/JSON path language: item method YES @@ -565,7 +565,7 @@ T834 SQL/JSON path language: wildcard member accessor YES T835 SQL/JSON path language: filter expressions YES T836 SQL/JSON path language: starts with predicate YES T837 SQL/JSON path language: regex_like predicate YES -T838 JSON_TABLE: PLAN DEFAULT clause NO +T838 JSON_TABLE: PLAN DEFAULT clause YES T839 Formatted cast of datetimes to/from character strings NO T840 Hex integer literals in SQL/JSON path language YES T851 SQL/JSON: optional keywords for default syntax YES -- 2.35.3
v1-0003-JSON_TABLE.patch (application/octet-stream, 160.3K) download From fe9dadcc27ef87b09921f3d766b8eb76a7e013c5 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:14:12 +0900 Subject: [PATCH v1 3/4] JSON_TABLE This feature allows jsonb data to be treated as a table and thus used in a FROM clause like other tabular data. Data can be selected from the jsonb using jsonpath expressions, and hoisted out of nested structures in the jsonb to form multiple rows, more or less like an outer join. This also adds the PLAN clauses for JSON_TABLE, which allow the user to specify how data from nested paths are joined, allowing considerable freedom in shaping the tabular output of JSON_TABLE. PLAN DEFAULT allows the user to specify the global strategies when dealing with sibling or child nested paths. The is often sufficient to achieve the necessary goal, and is considerably simpler than the full PLAN clause, which allows the user to specify the strategy to be used for each named nested path. Nikita Glukhov Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zhihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby. Discussion: https://postgr.es/m/7e2cb85d-24cf-4abb-30a5-1a33715959bd@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org --- doc/src/sgml/func.sgml | 448 ++++++++ src/backend/commands/explain.c | 8 +- src/backend/executor/execExprInterp.c | 5 + src/backend/executor/nodeTableFuncscan.c | 27 +- src/backend/nodes/makefuncs.c | 19 + src/backend/nodes/nodeFuncs.c | 30 + src/backend/parser/Makefile | 1 + src/backend/parser/gram.y | 475 +++++++- src/backend/parser/meson.build | 1 + src/backend/parser/parse_clause.c | 14 +- src/backend/parser/parse_expr.c | 35 +- src/backend/parser/parse_jsontable.c | 739 +++++++++++++ src/backend/parser/parse_relation.c | 5 +- src/backend/parser/parse_target.c | 3 + src/backend/utils/adt/jsonpath_exec.c | 543 ++++++++++ src/backend/utils/adt/ruleutils.c | 279 ++++- src/include/nodes/execnodes.h | 2 + src/include/nodes/makefuncs.h | 2 + src/include/nodes/parsenodes.h | 90 ++ src/include/nodes/primnodes.h | 61 +- src/include/parser/kwlist.h | 4 + src/include/parser/parse_clause.h | 3 + src/include/utils/jsonpath.h | 3 + src/test/regress/expected/json_sqljson.out | 6 + src/test/regress/expected/jsonb_sqljson.out | 1069 +++++++++++++++++++ src/test/regress/sql/json_sqljson.sql | 4 + src/test/regress/sql/jsonb_sqljson.sql | 629 +++++++++++ src/tools/pgindent/typedefs.list | 13 + 28 files changed, 4485 insertions(+), 33 deletions(-) create mode 100644 src/backend/parser/parse_jsontable.c diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index e7a018583a..74b017d2da 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -17144,6 +17144,454 @@ array w/o UK? | t </sect2> + <sect2 id="functions-sqljson-table"> + <title>JSON_TABLE</title> + <indexterm> + <primary>json_table</primary> + </indexterm> + + <para> + <function>json_table</function> is an SQL/JSON function which + queries <acronym>JSON</acronym> data + and presents the results as a relational view, which can be accessed as a + regular SQL table. You can only use <function>json_table</function> inside the + <literal>FROM</literal> clause of a <literal>SELECT</literal> statement. + </para> + + <para> + Taking JSON data as input, <function>json_table</function> uses + a path expression to extract a part of the provided data that + will be used as a <firstterm>row pattern</firstterm> for the + constructed view. Each SQL/JSON item at the top level of the row pattern serves + as the source for a separate row in the constructed relational view. + </para> + + <para> + To split the row pattern into columns, <function>json_table</function> + provides the <literal>COLUMNS</literal> clause that defines the + schema of the created view. For each column to be constructed, + this clause provides a separate path expression that evaluates + the row pattern, extracts a JSON item, and returns it as a + separate SQL value for the specified column. If the required value + is stored in a nested level of the row pattern, it can be extracted + using the <literal>NESTED PATH</literal> subclause. Joining the + columns returned by <literal>NESTED PATH</literal> can add multiple + new rows to the constructed view. Such rows are called + <firstterm>child rows</firstterm>, as opposed to the <firstterm>parent row</firstterm> + that generates them. + </para> + + <para> + The rows produced by <function>JSON_TABLE</function> are laterally + joined to the row that generated them, so you do not have to explicitly join + the constructed view with the original table holding <acronym>JSON</acronym> + data. Optionally, you can specify how to join the columns returned + by <literal>NESTED PATH</literal> using the <literal>PLAN</literal> clause. + </para> + + <para> + Each <literal>NESTED PATH</literal> clause can generate one or more + columns. Columns produced by <literal>NESTED PATH</literal>s at the + same level are considered to be <firstterm>siblings</firstterm>, + while a column produced by a <literal>NESTED PATH</literal> is + considered to be a child of the column produced by a + <literal>NESTED PATH</literal> or row expression at a higher level. + Sibling columns are always joined first. Once they are processed, + the resulting rows are joined to the parent row. + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional></literal> + </term> + <listitem> + <para> + The input data to query, the JSON path expression defining the query, + and an optional <literal>PASSING</literal> clause, which can provide data + values to the <replaceable>path_expression</replaceable>. + The result of the input data + evaluation is called the <firstterm>row pattern</firstterm>. The row + pattern is used as the source for row values in the constructed view. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>COLUMNS</literal>( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + The <literal>COLUMNS</literal> clause defining the schema of the + constructed view. In this clause, you must specify all the columns + to be filled with SQL/JSON items. + The <replaceable>json_table_column</replaceable> + expression has the following syntax variants: + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>name</replaceable> <replaceable>type</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional></literal> + </term> + <listitem> + + <para> + Inserts a single SQL/JSON item into each row of + the specified column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable> + and fills the column with produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>ON EMPTY</literal> and + <literal>ON ERROR</literal> clauses to define how to handle missing values + or structural errors. + <literal>WRAPPER</literal> and <literal>QUOTES</literal> clauses can only + be used with JSON, array, and composite types. + These clauses have the same syntax and semantics as for + <function>json_value</function> and <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> <literal>FORMAT</literal> <replaceable>json_representation</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a composite SQL/JSON + item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable> + and fills the column with produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>WRAPPER</literal>, <literal>QUOTES</literal>, + <literal>ON EMPTY</literal> and <literal>ON ERROR</literal> clauses + to define additional settings for the returned SQL/JSON items. + These clauses have the same syntax and semantics as + for <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> + <literal>EXISTS</literal> <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a boolean item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable>, + checks whether any SQL/JSON items were returned, and fills the column with + resulting boolean value, one for each row. + The specified <replaceable>type</replaceable> should have cast from + <type>boolean</type>. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + </para> + <para> + Optionally, you can add <literal>ON ERROR</literal> clause to define + error behavior. This clause has the same syntax and semantics as + for <function>json_exists</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>NESTED PATH</literal> <replaceable>json_path_specification</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> + <literal>COLUMNS</literal> ( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + Extracts SQL/JSON items from nested levels of the row pattern, + generates one or more columns as defined by the <literal>COLUMNS</literal> + subclause, and inserts the extracted SQL/JSON items into each row of these columns. + The <replaceable>json_table_column</replaceable> expression in the + <literal>COLUMNS</literal> subclause uses the same syntax as in the + parent <literal>COLUMNS</literal> clause. + </para> + + <para> + The <literal>NESTED PATH</literal> syntax is recursive, + so you can go down multiple nested levels by specifying several + <literal>NESTED PATH</literal> subclauses within each other. + It allows to unnest the hierarchy of JSON objects and arrays + in a single function invocation rather than chaining several + <function>JSON_TABLE</function> expressions in an SQL statement. + </para> + + <para> + You can use the <literal>PLAN</literal> clause to define how + to join the columns returned by <literal>NESTED PATH</literal> clauses. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <literal>FOR ORDINALITY</literal> + </term> + <listitem> + + <para> + Adds an ordinality column that provides sequential row numbering. + You can have only one ordinality column per table. Row numbering + is 1-based. For child rows that result from the <literal>NESTED PATH</literal> + clauses, the parent row number is repeated. + </para> + </listitem> + </varlistentry> + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>AS</literal> <replaceable>json_path_name</replaceable> + </term> + <listitem> + + <para> + The optional <replaceable>json_path_name</replaceable> serves as an + identifier of the provided <replaceable>json_path_specification</replaceable>. + The path name must be unique and distinct from the column names. + When using the <literal>PLAN</literal> clause, you must specify the names + for all the paths, including the row pattern. Each path name can appear in + the <literal>PLAN</literal> clause only once. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN</literal> ( <replaceable>json_table_plan</replaceable> ) + </term> + <listitem> + + <para> + Defines how to join the data returned by <literal>NESTED PATH</literal> + clauses to the constructed view. + </para> + <para> + To join columns with parent/child relationship, you can use: + </para> + <variablelist> + <varlistentry> + <term> + <literal>INNER</literal> + </term> + <listitem> + + <para> + Use <literal>INNER JOIN</literal>, so that the parent row + is omitted from the output if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>OUTER</literal> + </term> + <listitem> + + <para> + Use <literal>LEFT OUTER JOIN</literal>, so that the parent row + is always included into the output even if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>, with NULL values + inserted into the child columns if the corresponding + values are missing. + </para> + <para> + This is the default option for joining columns with parent/child relationship. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para> + To join sibling columns, you can use: + </para> + + <variablelist> + <varlistentry> + <term> + <literal>UNION</literal> + </term> + <listitem> + + <para> + Generate one row for each value produced by each of the sibling + columns. The columns from the other siblings are set to null. + </para> + <para> + This is the default option for joining sibling columns. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>CROSS</literal> + </term> + <listitem> + + <para> + Generate one row for each combination of values from the sibling columns. + </para> + </listitem> + </varlistentry> + + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN DEFAULT</literal> ( <literal><replaceable>OUTER | INNER</replaceable> <optional>, <replaceable>UNION | CROSS</replaceable> </optional></literal> ) + </term> + <listitem> + <para> + The terms can also be specified in reverse order. The + <literal>INNER</literal> or <literal>OUTER</literal> option defines the + joining plan for parent/child columns, while <literal>UNION</literal> or + <literal>CROSS</literal> affects joins of sibling columns. This form + of <literal>PLAN</literal> overrides the default plan for + all columns at once. Even though the path names are not included in the + <literal>PLAN DEFAULT</literal> form, to conform to the SQL/JSON standard + they must be provided for all the paths if the <literal>PLAN</literal> + clause is used. + </para> + <para> + <literal>PLAN DEFAULT</literal> is simpler than specifying a complete + <literal>PLAN</literal>, and is often all that is required to get the desired + output. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para>Examples</para> + + <para> + In these examples the following small table storing some JSON data will be used: +<programlisting> +CREATE TABLE my_films ( js jsonb ); + +INSERT INTO my_films VALUES ( +'{ "favorites" : [ + { "kind" : "comedy", "films" : [ + { "title" : "Bananas", + "director" : "Woody Allen"}, + { "title" : "The Dinner Game", + "director" : "Francis Veber" } ] }, + { "kind" : "horror", "films" : [ + { "title" : "Psycho", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "thriller", "films" : [ + { "title" : "Vertigo", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "drama", "films" : [ + { "title" : "Yojimbo", + "director" : "Akira Kurosawa" } ] } + ] }'); +</programlisting> + </para> + <para> + Query the <structname>my_films</structname> table holding + some JSON data about the films and create a view that + distributes the film genre, title, and director between separate columns: +<screen> +SELECT jt.* FROM + my_films, + JSON_TABLE ( js, '$.favorites[*]' COLUMNS ( + id FOR ORDINALITY, + kind text PATH '$.kind', + NESTED PATH '$.films[*]' COLUMNS ( + title text PATH '$.title', + director text PATH '$.director'))) AS jt; +----+----------+------------------+------------------- + id | kind | title | director +----+----------+------------------+------------------- + 1 | comedy | Bananas | Woody Allen + 1 | comedy | The Dinner Game | Francis Veber + 2 | horror | Psycho | Alfred Hitchcock + 3 | thriller | Vertigo | Alfred Hitchcock + 4 | drama | Yojimbo | Akira Kurosawa + (5 rows) +</screen> + </para> + + <para> + Find a director that has done films in two different genres: +<screen> +SELECT + director1 AS director, title1, kind1, title2, kind2 +FROM + my_films, + JSON_TABLE ( js, '$.favorites' AS favs COLUMNS ( + NESTED PATH '$[*]' AS films1 COLUMNS ( + kind1 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film1 COLUMNS ( + title1 text PATH '$.title', + director1 text PATH '$.director') + ), + NESTED PATH '$[*]' AS films2 COLUMNS ( + kind2 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film2 COLUMNS ( + title2 text PATH '$.title', + director2 text PATH '$.director' + ) + ) + ) + PLAN (favs OUTER ((films1 INNER film1) CROSS (films2 INNER film2))) + ) AS jt + WHERE kind1 > kind2 AND director1 = director2; + + director | title1 | kind1 | title2 | kind2 +------------------+---------+----------+--------+-------- + Alfred Hitchcock | Vertigo | thriller | Psycho | horror +(1 row) +</screen> + </para> + </sect2> + <sect2 id="functions-sqljson-path"> <title>The SQL/JSON Path Language</title> diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 15f9bddcdf..539eac8991 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -3862,7 +3862,13 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es) break; case T_TableFuncScan: Assert(rte->rtekind == RTE_TABLEFUNC); - objectname = "xmltable"; + if (rte->tablefunc) + if (rte->tablefunc->functype == TFT_XMLTABLE) + objectname = "xmltable"; + else /* Must be TFT_JSON_TABLE */ + objectname = "json_table"; + else + objectname = NULL; objecttag = "Table Function Name"; break; case T_ValuesScan: diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 8d57ac7a92..7c7d2a4c70 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -4303,6 +4303,11 @@ ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) break; } + case JSON_TABLE_OP: + res = item; + resnull = false; + break; + default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); *op->resnull = true; diff --git a/src/backend/executor/nodeTableFuncscan.c b/src/backend/executor/nodeTableFuncscan.c index 791cbd2372..085a612533 100644 --- a/src/backend/executor/nodeTableFuncscan.c +++ b/src/backend/executor/nodeTableFuncscan.c @@ -28,6 +28,7 @@ #include "miscadmin.h" #include "nodes/execnodes.h" #include "utils/builtins.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/xml.h" @@ -161,8 +162,9 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) scanstate->ss.ps.qual = ExecInitQual(node->scan.plan.qual, &scanstate->ss.ps); - /* Only XMLTABLE is supported currently */ - scanstate->routine = &XmlTableRoutine; + /* Only XMLTABLE and JSON_TABLE are supported currently */ + scanstate->routine = + tf->functype == TFT_XMLTABLE ? &XmlTableRoutine : &JsonbTableRoutine; scanstate->perTableCxt = AllocSetContextCreate(CurrentMemoryContext, @@ -182,6 +184,10 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) ExecInitExprList(tf->colexprs, (PlanState *) scanstate); scanstate->coldefexprs = ExecInitExprList(tf->coldefexprs, (PlanState *) scanstate); + scanstate->colvalexprs = + ExecInitExprList(tf->colvalexprs, (PlanState *) scanstate); + scanstate->passingvalexprs = + ExecInitExprList(tf->passingvalexprs, (PlanState *) scanstate); scanstate->notnulls = tf->notnulls; @@ -381,14 +387,17 @@ tfuncInitialize(TableFuncScanState *tstate, ExprContext *econtext, Datum doc) routine->SetNamespace(tstate, ns_name, ns_uri); } - /* Install the row filter expression into the table builder context */ - value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); - if (isnull) - ereport(ERROR, - (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), - errmsg("row filter expression must not be null"))); + if (routine->SetRowFilter) + { + /* Install the row filter expression into the table builder context */ + value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); + if (isnull) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("row filter expression must not be null"))); - routine->SetRowFilter(tstate, TextDatumGetCString(value)); + routine->SetRowFilter(tstate, TextDatumGetCString(value)); + } /* * Install the column filter expressions into the table builder context. diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 5dc3aa2e9f..84eb33b1db 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -874,6 +874,25 @@ makeJsonBehavior(JsonBehaviorType type, Node *default_expr) return behavior; } +/* + * makeJsonTableJoinedPlan - + * creates a joined JsonTablePlan node + */ +Node * +makeJsonTableJoinedPlan(JsonTablePlanJoinType type, Node *plan1, Node *plan2, + int location) +{ + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_JOINED; + n->join_type = type; + n->plan1 = castNode(JsonTablePlan, plan1); + n->plan2 = castNode(JsonTablePlan, plan2); + n->location = location; + + return (Node *) n; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 3e3fa06b02..f2782b582d 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -2631,6 +2631,10 @@ expression_tree_walker_impl(Node *node, return true; if (WALK(tf->coldefexprs)) return true; + if (WALK(tf->colvalexprs)) + return true; + if (WALK(tf->passingvalexprs)) + return true; } break; default: @@ -3699,6 +3703,8 @@ expression_tree_mutator_impl(Node *node, MUTATE(newnode->rowexpr, tf->rowexpr, Node *); MUTATE(newnode->colexprs, tf->colexprs, List *); MUTATE(newnode->coldefexprs, tf->coldefexprs, List *); + MUTATE(newnode->colvalexprs, tf->colvalexprs, List *); + MUTATE(newnode->passingvalexprs, tf->passingvalexprs, List *); return (Node *) newnode; } break; @@ -4130,6 +4136,30 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonTable: + { + JsonTable *jt = (JsonTable *) node; + + if (WALK(jt->common)) + return true; + if (WALK(jt->columns)) + return true; + } + break; + case T_JsonTableColumn: + { + JsonTableColumn *jtc = (JsonTableColumn *) node; + + if (WALK(jtc->typeName)) + return true; + if (WALK(jtc->on_empty)) + return true; + if (WALK(jtc->on_error)) + return true; + if (jtc->coltype == JTC_NESTED && WALK(jtc->columns)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile index 9f1c4022bb..f4c0cc7f10 100644 --- a/src/backend/parser/Makefile +++ b/src/backend/parser/Makefile @@ -23,6 +23,7 @@ OBJS = \ parse_enr.o \ parse_expr.o \ parse_func.o \ + parse_jsontable.o \ parse_merge.o \ parse_node.o \ parse_oper.o \ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index f7f0d5e8bc..cb7be4e907 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -656,22 +656,48 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_aggregate_func json_api_common_syntax json_argument + json_table + json_table_column_definition + json_table_ordinality_column_definition + json_table_regular_column_definition + json_table_formatted_column_definition + json_table_exists_column_definition + json_table_nested_columns + json_table_plan_clause_opt + json_table_specific_plan + json_table_plan + json_table_plan_simple + json_table_plan_parent_child + json_table_plan_outer + json_table_plan_inner + json_table_plan_sibling + json_table_plan_union + json_table_plan_cross + json_table_plan_primary + json_table_default_plan %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt json_arguments json_passing_clause_opt + json_table_columns_clause + json_table_column_definition_list %type <str> json_as_path_name_clause_opt + json_table_column_path_specification_clause_opt %type <ival> json_encoding_clause_opt json_predicate_type_constraint + json_table_default_plan_choices + json_table_default_plan_inner_outer + json_table_default_plan_union_cross json_wrapper_behavior %type <jsbehavior> json_value_behavior json_query_behavior json_exists_behavior + json_table_behavior %type <js_quotes> json_quotes_clause_opt @@ -742,7 +768,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG - JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_TABLE JSON_VALUE KEY KEYS KEEP @@ -753,8 +779,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE - NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE - NORMALIZE NORMALIZED + NAME_P NAMES NATIONAL NATURAL NCHAR NESTED NEW NEXT NFC NFD NFKC NFKD NO + NONE NORMALIZE NORMALIZED NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC @@ -762,8 +788,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER - PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD - PLACING PLANS POLICY + PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH + PLACING PLAN PLANS POLICY POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION @@ -871,6 +897,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); * Using the same precedence as IDENT seems right for the reasons given above. */ %nonassoc UNBOUNDED /* ideally would have same precedence as IDENT */ +%nonassoc COLUMNS %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' @@ -893,6 +920,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL +%nonassoc json_table_column +%nonassoc NESTED +%left PATH %% /* @@ -13334,6 +13364,21 @@ table_ref: relation_expr opt_alias_clause $2->alias = $4; $$ = (Node *) $2; } + | json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $1); + + jt->alias = $2; + $$ = (Node *) jt; + } + | LATERAL_P json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $2); + + jt->alias = $3; + jt->lateral = true; + $$ = (Node *) jt; + } ; @@ -13901,6 +13946,8 @@ xmltable_column_option_el: { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(true), @1); } | NULL_P { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(false), @1); } + | PATH b_expr + { $$ = makeDefElem("path", $2, @1); } ; xml_namespace_list: @@ -16683,6 +16730,11 @@ json_value_behavior: | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } ; +json_table_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + ; + /* ARRAY is a noise word */ json_wrapper_behavior: WITHOUT WRAPPER { $$ = JSW_NONE; } @@ -16704,6 +16756,411 @@ json_quotes_clause_opt: | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } ; +json_table: + JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + json_table_behavior ON ERROR_P + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_columns_clause: + COLUMNS '(' json_table_column_definition_list ')' { $$ = $3; } + ; + +json_table_column_definition_list: + json_table_column_definition + { $$ = list_make1($1); } + | json_table_column_definition_list ',' json_table_column_definition + { $$ = lappend($1, $3); } + ; + +json_table_column_definition: + json_table_ordinality_column_definition %prec json_table_column + | json_table_regular_column_definition %prec json_table_column + | json_table_formatted_column_definition %prec json_table_column + | json_table_exists_column_definition %prec json_table_column + | json_table_nested_columns + ; + +json_table_ordinality_column_definition: + ColId FOR ORDINALITY + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FOR_ORDINALITY; + n->name = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_regular_column_definition: + ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_exists_column_definition: + ColId Typename + EXISTS json_table_column_path_specification_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + EXISTS json_table_column_path_specification_clause_opt + json_exists_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_column_path_specification_clause_opt: + PATH Sconst { $$ = $2; } + | /* EMPTY */ %prec json_table_column { $$ = NULL; } + ; + +json_table_formatted_column_definition: + ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->on_error = $12; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_nested_columns: + NESTED path_opt Sconst + json_as_path_name_clause_opt + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = $4; + n->columns = $5; + n->location = @1; + $$ = (Node *) n; + } + ; + +path_opt: + PATH { } + | /* EMPTY */ { } + ; + +json_table_plan_clause_opt: + json_table_specific_plan { $$ = $1; } + | json_table_default_plan { $$ = $1; } + | /* EMPTY */ { $$ = NULL; } + ; + +json_table_specific_plan: + PLAN '(' json_table_plan ')' { $$ = $3; } + ; + +json_table_plan: + json_table_plan_simple + | json_table_plan_parent_child + | json_table_plan_sibling + ; + +json_table_plan_simple: + name + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_SIMPLE; + n->pathname = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_plan_parent_child: + json_table_plan_outer + | json_table_plan_inner + ; + +json_table_plan_outer: + json_table_plan_simple OUTER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_OUTER, $1, $3, @1); } + ; + +json_table_plan_inner: + json_table_plan_simple INNER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_INNER, $1, $3, @1); } + ; + +json_table_plan_sibling: + json_table_plan_union + | json_table_plan_cross + ; + +json_table_plan_union: + json_table_plan_primary UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + | json_table_plan_union UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + ; + +json_table_plan_cross: + json_table_plan_primary CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + | json_table_plan_cross CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + ; + +json_table_plan_primary: + json_table_plan_simple { $$ = $1; } + | '(' json_table_plan ')' + { + castNode(JsonTablePlan, $2)->location = @1; + $$ = $2; + } + ; + +json_table_default_plan: + PLAN DEFAULT '(' json_table_default_plan_choices ')' + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_DEFAULT; + n->join_type = $4; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_default_plan_choices: + json_table_default_plan_inner_outer { $$ = $1 | JSTPJ_UNION; } + | json_table_default_plan_inner_outer ',' + json_table_default_plan_union_cross { $$ = $1 | $3; } + | json_table_default_plan_union_cross { $$ = $1 | JSTPJ_OUTER; } + | json_table_default_plan_union_cross ',' + json_table_default_plan_inner_outer { $$ = $1 | $3; } + ; + +json_table_default_plan_inner_outer: + INNER_P { $$ = JSTPJ_INNER; } + | OUTER_P { $$ = JSTPJ_OUTER; } + ; + +json_table_default_plan_union_cross: + UNION { $$ = JSTPJ_UNION; } + | CROSS { $$ = JSTPJ_CROSS; } + ; + json_returning_clause_opt: RETURNING Typename { @@ -17442,6 +17899,7 @@ unreserved_keyword: | MOVE | NAME_P | NAMES + | NESTED | NEW | NEXT | NFC @@ -17476,6 +17934,8 @@ unreserved_keyword: | PARTITION | PASSING | PASSWORD + | PATH + | PLAN | PLANS | POLICY | PRECEDING @@ -17640,6 +18100,7 @@ col_name_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | LEAST | NATIONAL @@ -18008,6 +18469,7 @@ bare_label_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | KEEP | KEY @@ -18047,6 +18509,7 @@ bare_label_keyword: | NATIONAL | NATURAL | NCHAR + | NESTED | NEW | NEXT | NFC @@ -18091,7 +18554,9 @@ bare_label_keyword: | PARTITION | PASSING | PASSWORD + | PATH | PLACING + | PLAN | PLANS | POLICY | POSITION diff --git a/src/backend/parser/meson.build b/src/backend/parser/meson.build index 94517a7f7d..0f0e4ac7ca 100644 --- a/src/backend/parser/meson.build +++ b/src/backend/parser/meson.build @@ -10,6 +10,7 @@ backend_sources += files( 'parse_enr.c', 'parse_expr.c', 'parse_func.c', + 'parse_jsontable.c', 'parse_merge.c', 'parse_node.c', 'parse_oper.c', diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index f61f794755..b44ff44991 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -697,7 +697,11 @@ transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf) char **names; int colno; - /* Currently only XMLTABLE is supported */ + /* + * Currently we only support XMLTABLE here. See transformJsonTable() for + * JSON_TABLE support. + */ + tf->functype = TFT_XMLTABLE; constructName = "XMLTABLE"; docType = XMLOID; @@ -1104,13 +1108,17 @@ transformFromClauseItem(ParseState *pstate, Node *n, rtr->rtindex = nsitem->p_rtindex; return (Node *) rtr; } - else if (IsA(n, RangeTableFunc)) + else if (IsA(n, RangeTableFunc) || IsA(n, JsonTable)) { /* table function is like a plain relation */ RangeTblRef *rtr; ParseNamespaceItem *nsitem; - nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + if (IsA(n, RangeTableFunc)) + nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + else + nsitem = transformJsonTable(pstate, (JsonTable *) n); + *top_nsitem = nsitem; *namespace = list_make1(nsitem); rtr = makeNode(RangeTblRef); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 9882f0c217..b331feda0d 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4250,7 +4250,7 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) JsonFormatType format; char *constructName; - if (func->common->pathname) + if (func->common->pathname && func->op != JSON_TABLE_OP) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("JSON_TABLE path name is not allowed here"), @@ -4269,6 +4269,9 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) case JSON_EXISTS_OP: constructName = "JSON_EXISTS()"; break; + case JSON_TABLE_OP: + constructName = "JSON_TABLE()"; + break; default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); break; @@ -4307,14 +4310,19 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) transformJsonPassingArgs(pstate, format, func->common->passing, &jsexpr->passing_values, &jsexpr->passing_names); - if (func->op != JSON_EXISTS_OP) + if (func->op != JSON_EXISTS_OP && func->op != JSON_TABLE_OP) jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, JSON_BEHAVIOR_NULL); - jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, - func->op == JSON_EXISTS_OP ? - JSON_BEHAVIOR_FALSE : - JSON_BEHAVIOR_NULL); + if (func->op == JSON_EXISTS_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_FALSE); + else if (func->op == JSON_TABLE_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_EMPTY); + else + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_NULL); return jsexpr; } @@ -4615,6 +4623,21 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->result_coercion->expr = NULL; } break; + + case JSON_TABLE_OP: + jsexpr->returning = makeNode(JsonReturning); + jsexpr->returning->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + jsexpr->returning->typid = exprType(contextItemExpr); + jsexpr->returning->typmod = -1; + + if (jsexpr->returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("JSON_TABLE() is not yet implemented for the json type"), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + break; } if (exprType(contextItemExpr) != JSONBOID) diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c new file mode 100644 index 0000000000..a7802e0499 --- /dev/null +++ b/src/backend/parser/parse_jsontable.c @@ -0,0 +1,739 @@ +/*------------------------------------------------------------------------- + * + * parse_jsontable.c + * parsing of JSON_TABLE + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/parser/parse_jsontable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/pg_collation.h" +#include "catalog/pg_type.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "optimizer/optimizer.h" +#include "parser/parse_clause.h" +#include "parser/parse_collate.h" +#include "parser/parse_expr.h" +#include "parser/parse_relation.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/json.h" +#include "utils/lsyscache.h" + +/* Context for JSON_TABLE transformation */ +typedef struct JsonTableParseContext +{ + ParseState *pstate; /* parsing state */ + JsonTable *table; /* untransformed node */ + TableFunc *tablefunc; /* transformed node */ + List *pathNames; /* list of all path and columns names */ + int pathNameId; /* path name id counter */ + Oid contextItemTypid; /* type oid of context item (json/jsonb) */ +} JsonTableParseContext; + +static JsonTableParent *transformJsonTableColumns(JsonTableParseContext *cxt, + JsonTablePlan *plan, + List *columns, + char *pathSpec, + char **pathName, + int location); + +static Node * +makeStringConst(char *str, int location) +{ + A_Const *n = makeNode(A_Const); + + n->val.node.type = T_String; + n->val.sval.sval = str; + n->location = location; + + return (Node *) n; +} + +/* + * Transform JSON_TABLE column + * - regular column into JSON_VALUE() + * - FORMAT JSON column into JSON_QUERY() + * - EXISTS column into JSON_EXISTS() + */ +static Node * +transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, + List *passingArgs, bool errorOnError) +{ + JsonFuncExpr *jfexpr = makeNode(JsonFuncExpr); + JsonCommon *common = makeNode(JsonCommon); + JsonOutput *output = makeNode(JsonOutput); + char *pathspec; + JsonFormat *default_format; + + jfexpr->op = + jtc->coltype == JTC_REGULAR ? JSON_VALUE_OP : + jtc->coltype == JTC_EXISTS ? JSON_EXISTS_OP : JSON_QUERY_OP; + jfexpr->common = common; + jfexpr->output = output; + jfexpr->on_empty = jtc->on_empty; + jfexpr->on_error = jtc->on_error; + if (!jfexpr->on_error && errorOnError) + jfexpr->on_error = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); + jfexpr->omit_quotes = jtc->omit_quotes; + jfexpr->wrapper = jtc->wrapper; + jfexpr->location = jtc->location; + + output->typeName = jtc->typeName; + output->returning = makeNode(JsonReturning); + output->returning->format = jtc->format; + + default_format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + + common->pathname = NULL; + common->expr = makeJsonValueExpr((Expr *) contextItemExpr, default_format); + common->passing = passingArgs; + + if (jtc->pathspec) + pathspec = jtc->pathspec; + else + { + /* Construct default path as '$."column_name"' */ + StringInfoData path; + + initStringInfo(&path); + + appendStringInfoString(&path, "$."); + escape_json(&path, jtc->name); + + pathspec = path.data; + } + + common->pathspec = makeStringConst(pathspec, -1); + + return (Node *) jfexpr; +} + +static bool +isJsonTablePathNameDuplicate(JsonTableParseContext *cxt, const char *pathname) +{ + ListCell *lc; + + foreach(lc, cxt->pathNames) + { + if (!strcmp(pathname, (const char *) lfirst(lc))) + return true; + } + + return false; +} + +/* Register the column name in the path name list. */ +static void +registerJsonTableColumn(JsonTableParseContext *cxt, char *colname) +{ + if (isJsonTablePathNameDuplicate(cxt, colname)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_ALIAS), + errmsg("duplicate JSON_TABLE column name: %s", colname), + errhint("JSON_TABLE column names must be distinct from one another."))); + + cxt->pathNames = lappend(cxt->pathNames, colname); +} + +/* Recursively register all nested column names in the path name list. */ +static void +registerAllJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED) + { + if (jtc->pathname) + registerJsonTableColumn(cxt, jtc->pathname); + + registerAllJsonTableColumns(cxt, jtc->columns); + } + else + { + registerJsonTableColumn(cxt, jtc->name); + } + } +} + +/* Generate a new unique JSON_TABLE path name. */ +static char * +generateJsonTablePathName(JsonTableParseContext *cxt) +{ + char namebuf[32]; + char *name = namebuf; + + do + { + snprintf(namebuf, sizeof(namebuf), "json_table_path_%d", + ++cxt->pathNameId); + } while (isJsonTablePathNameDuplicate(cxt, name)); + + name = pstrdup(name); + cxt->pathNames = lappend(cxt->pathNames, name); + + return name; +} + +/* Collect sibling path names from plan to the specified list. */ +static void +collectSiblingPathsInJsonTablePlan(JsonTablePlan *plan, List **paths) +{ + if (plan->plan_type == JSTP_SIMPLE) + *paths = lappend(*paths, plan->pathname); + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + *paths = lappend(*paths, plan->plan1->pathname); + } + else if (plan->join_type == JSTPJ_CROSS || + plan->join_type == JSTPJ_UNION) + { + collectSiblingPathsInJsonTablePlan(plan->plan1, paths); + collectSiblingPathsInJsonTablePlan(plan->plan2, paths); + } + else + elog(ERROR, "invalid JSON_TABLE join type %d", + plan->join_type); + } +} + +/* + * Validate child JSON_TABLE plan by checking that: + * - all nested columns have path names specified + * - all nested columns have corresponding node in the sibling plan + * - plan does not contain duplicate or extra nodes + */ +static void +validateJsonTableChildPlan(ParseState *pstate, JsonTablePlan *plan, + List *columns) +{ + ListCell *lc1; + List *siblings = NIL; + int nchildren = 0; + + if (plan) + collectSiblingPathsInJsonTablePlan(plan, &siblings); + + foreach(lc1, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc1)); + + if (jtc->coltype == JTC_NESTED) + { + ListCell *lc2; + bool found = false; + + if (!jtc->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("nested JSON_TABLE columns must contain an explicit AS pathname specification if an explicit PLAN clause is used"), + parser_errposition(pstate, jtc->location))); + + /* find nested path name in the list of sibling path names */ + foreach(lc2, siblings) + { + if ((found = !strcmp(jtc->pathname, lfirst(lc2)))) + break; + } + + if (!found) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node for nested path %s was not found in plan.", jtc->pathname), + parser_errposition(pstate, jtc->location))); + + nchildren++; + } + } + + if (list_length(siblings) > nchildren) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node contains some extra or duplicate sibling nodes."), + parser_errposition(pstate, plan ? plan->location : -1))); +} + +static JsonTableColumn * +findNestedJsonTableColumn(List *columns, const char *pathname) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED && + jtc->pathname && + !strcmp(jtc->pathname, pathname)) + return jtc; + } + + return NULL; +} + +static Node * +transformNestedJsonTableColumn(JsonTableParseContext *cxt, JsonTableColumn *jtc, + JsonTablePlan *plan) +{ + JsonTableParent *node; + char *pathname = jtc->pathname; + + node = transformJsonTableColumns(cxt, plan, jtc->columns, jtc->pathspec, + &pathname, jtc->location); + + return (Node *) node; +} + +static Node * +makeJsonTableSiblingJoin(bool cross, Node *lnode, Node *rnode) +{ + JsonTableSibling *join = makeNode(JsonTableSibling); + + join->larg = lnode; + join->rarg = rnode; + join->cross = cross; + + return (Node *) join; +} + +/* + * Recursively transform child JSON_TABLE plan. + * + * Default plan is transformed into a cross/union join of its nested columns. + * Simple and outer/inner plans are transformed into a JsonTableParent by + * finding and transforming corresponding nested column. + * Sibling plans are recursively transformed into a JsonTableSibling. + */ +static Node * +transformJsonTableChildPlan(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns) +{ + JsonTableColumn *jtc = NULL; + + if (!plan || plan->plan_type == JSTP_DEFAULT) + { + /* unspecified or default plan */ + Node *res = NULL; + ListCell *lc; + bool cross = plan && (plan->join_type & JSTPJ_CROSS); + + /* transform all nested columns into cross/union join */ + foreach(lc, columns) + { + JsonTableColumn *col = castNode(JsonTableColumn, lfirst(lc)); + Node *node; + + if (col->coltype != JTC_NESTED) + continue; + + node = transformNestedJsonTableColumn(cxt, col, plan); + + /* join transformed node with previous sibling nodes */ + res = res ? makeJsonTableSiblingJoin(cross, res, node) : node; + } + + return res; + } + else if (plan->plan_type == JSTP_SIMPLE) + { + jtc = findNestedJsonTableColumn(columns, plan->pathname); + } + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + jtc = findNestedJsonTableColumn(columns, plan->plan1->pathname); + } + else + { + Node *node1 = transformJsonTableChildPlan(cxt, plan->plan1, + columns); + Node *node2 = transformJsonTableChildPlan(cxt, plan->plan2, + columns); + + return makeJsonTableSiblingJoin(plan->join_type == JSTPJ_CROSS, + node1, node2); + } + } + else + elog(ERROR, "invalid JSON_TABLE plan type %d", plan->plan_type); + + if (!jtc) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name was %s not found in nested columns list.", + plan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + return transformNestedJsonTableColumn(cxt, jtc, plan); +} + +/* Check whether type is json/jsonb, array, or record. */ +static bool +typeIsComposite(Oid typid) +{ + char typtype; + + if (typid == JSONOID || + typid == JSONBOID || + typid == RECORDOID || + type_is_array(typid)) + return true; + + typtype = get_typtype(typid); + + if (typtype == TYPTYPE_COMPOSITE) + return true; + + if (typtype == TYPTYPE_DOMAIN) + return typeIsComposite(getBaseType(typid)); + + return false; +} + +/* Append transformed non-nested JSON_TABLE columns to the TableFunc node */ +static void +appendJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *col; + ParseState *pstate = cxt->pstate; + JsonTable *jt = cxt->table; + TableFunc *tf = cxt->tablefunc; + bool errorOnError = jt->on_error && + jt->on_error->btype == JSON_BEHAVIOR_ERROR; + + foreach(col, columns) + { + JsonTableColumn *rawc = castNode(JsonTableColumn, lfirst(col)); + Oid typid; + int32 typmod; + Node *colexpr; + + if (rawc->name) + { + /* make sure column names are unique */ + ListCell *colname; + + foreach(colname, tf->colnames) + if (!strcmp((const char *) colname, rawc->name)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("column name \"%s\" is not unique", + rawc->name), + parser_errposition(pstate, rawc->location))); + + tf->colnames = lappend(tf->colnames, + makeString(pstrdup(rawc->name))); + } + + /* + * Determine the type and typmod for the new column. FOR ORDINALITY + * columns are INTEGER by standard; the others are user-specified. + */ + switch (rawc->coltype) + { + case JTC_FOR_ORDINALITY: + colexpr = NULL; + typid = INT4OID; + typmod = -1; + break; + + case JTC_REGULAR: + typenameTypeIdAndMod(pstate, rawc->typeName, &typid, &typmod); + + /* + * Use implicit FORMAT JSON for composite types (arrays and + * records) + */ + if (typeIsComposite(typid)) + rawc->coltype = JTC_FORMATTED; + else if (rawc->wrapper != JSW_NONE) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use WITH WRAPPER clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + else if (rawc->omit_quotes) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use OMIT QUOTES clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + + /* FALLTHROUGH */ + case JTC_EXISTS: + case JTC_FORMATTED: + { + Node *je; + CaseTestExpr *param = makeNode(CaseTestExpr); + + param->collation = InvalidOid; + param->typeId = cxt->contextItemTypid; + param->typeMod = -1; + + je = transformJsonTableColumn(rawc, (Node *) param, + NIL, errorOnError); + + colexpr = transformExpr(pstate, je, EXPR_KIND_FROM_FUNCTION); + assign_expr_collations(pstate, colexpr); + + typid = exprType(colexpr); + typmod = exprTypmod(colexpr); + break; + } + + case JTC_NESTED: + continue; + + default: + elog(ERROR, "unknown JSON_TABLE column type: %d", rawc->coltype); + break; + } + + tf->coltypes = lappend_oid(tf->coltypes, typid); + tf->coltypmods = lappend_int(tf->coltypmods, typmod); + tf->colcollations = lappend_oid(tf->colcollations, get_typcollation(typid)); + tf->colvalexprs = lappend(tf->colvalexprs, colexpr); + } +} + +/* + * Create transformed JSON_TABLE parent plan node by appending all non-nested + * columns to the TableFunc node and remembering their indices in the + * colvalexprs list. + */ +static JsonTableParent * +makeParentJsonTableNode(JsonTableParseContext *cxt, char *pathSpec, char *pathName, + List *columns) +{ + JsonTableParent *node = makeNode(JsonTableParent); + + node->path = makeNode(JsonTablePath); + node->path->value = makeConst(JSONPATHOID, -1, InvalidOid, -1, + DirectFunctionCall1(jsonpath_in, + CStringGetDatum(pathSpec)), + false, false); + if (pathName) + node->path->name = pstrdup(pathName); + + /* save start of column range */ + node->colMin = list_length(cxt->tablefunc->colvalexprs); + + appendJsonTableColumns(cxt, columns); + + /* save end of column range */ + node->colMax = list_length(cxt->tablefunc->colvalexprs) - 1; + + node->errorOnError = + cxt->table->on_error && + cxt->table->on_error->btype == JSON_BEHAVIOR_ERROR; + + return node; +} + +static JsonTableParent * +transformJsonTableColumns(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns, char *pathSpec, char **pathName, + int location) +{ + JsonTableParent *node; + JsonTablePlan *childPlan; + bool defaultPlan = !plan || plan->plan_type == JSTP_DEFAULT; + + if (!*pathName) + { + if (cxt->table->plan) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE expression"), + errdetail("JSON_TABLE columns must contain " + "explicit AS pathname specification if " + "explicit PLAN clause is used"), + parser_errposition(cxt->pstate, location))); + + *pathName = generateJsonTablePathName(cxt); + } + + if (defaultPlan) + childPlan = plan; + else + { + /* validate parent and child plans */ + JsonTablePlan *parentPlan; + + if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type != JSTPJ_INNER && + plan->join_type != JSTPJ_OUTER) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Expected INNER or OUTER JSON_TABLE plan node."), + parser_errposition(cxt->pstate, plan->location))); + + parentPlan = plan->plan1; + childPlan = plan->plan2; + + Assert(parentPlan->plan_type != JSTP_JOINED); + Assert(parentPlan->pathname); + } + else + { + parentPlan = plan; + childPlan = NULL; + } + + if (strcmp(parentPlan->pathname, *pathName)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name mismatch: expected %s but %s is given.", + *pathName, parentPlan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + validateJsonTableChildPlan(cxt->pstate, childPlan, columns); + } + + /* transform only non-nested columns */ + node = makeParentJsonTableNode(cxt, pathSpec, *pathName, columns); + + if (childPlan || defaultPlan) + { + /* transform recursively nested columns */ + node->child = transformJsonTableChildPlan(cxt, childPlan, columns); + if (node->child) + node->outerJoin = !plan || (plan->join_type & JSTPJ_OUTER); + /* else: default plan case, no children found */ + } + + return node; +} + +/* + * transformJsonTable - + * Transform a raw JsonTable into TableFunc. + * + * Transform the document-generating expression, the row-generating expression, + * the column-generating expressions, and the default value expressions. + */ +ParseNamespaceItem * +transformJsonTable(ParseState *pstate, JsonTable *jt) +{ + JsonTableParseContext cxt; + TableFunc *tf = makeNode(TableFunc); + JsonFuncExpr *jfe = makeNode(JsonFuncExpr); + JsonExpr *je; + JsonTablePlan *plan = jt->plan; + JsonCommon *jscommon; + char *rootPathName = jt->common->pathname; + char *rootPath; + bool is_lateral; + + cxt.pstate = pstate; + cxt.table = jt; + cxt.tablefunc = tf; + cxt.pathNames = NIL; + cxt.pathNameId = 0; + + if (rootPathName) + registerJsonTableColumn(&cxt, rootPathName); + + registerAllJsonTableColumns(&cxt, jt->columns); + +#if 0 /* XXX it' unclear from the standard whether + * root path name is mandatory or not */ + if (plan && plan->plan_type != JSTP_DEFAULT && !rootPathName) + { + /* Assign root path name and create corresponding plan node */ + JsonTablePlan *rootNode = makeNode(JsonTablePlan); + JsonTablePlan *rootPlan = (JsonTablePlan *) + makeJsonTableJoinedPlan(JSTPJ_OUTER, (Node *) rootNode, + (Node *) plan, jt->location); + + rootPathName = generateJsonTablePathName(&cxt); + + rootNode->plan_type = JSTP_SIMPLE; + rootNode->pathname = rootPathName; + + plan = rootPlan; + } +#endif + + jscommon = copyObject(jt->common); + jscommon->pathspec = makeStringConst(pstrdup("$"), -1); + + jfe->op = JSON_TABLE_OP; + jfe->common = jscommon; + jfe->on_error = jt->on_error; + jfe->location = jt->common->location; + + /* + * We make lateral_only names of this level visible, whether or not the + * RangeTableFunc is explicitly marked LATERAL. This is needed for SQL + * spec compliance and seems useful on convenience grounds for all + * functions in FROM. + * + * (LATERAL can't nest within a single pstate level, so we don't need + * save/restore logic here.) + */ + Assert(!pstate->p_lateral_active); + pstate->p_lateral_active = true; + + tf->functype = TFT_JSON_TABLE; + tf->docexpr = transformExpr(pstate, (Node *) jfe, EXPR_KIND_FROM_FUNCTION); + + cxt.contextItemTypid = exprType(tf->docexpr); + + if (!IsA(jt->common->pathspec, A_Const) || + castNode(A_Const, jt->common->pathspec)->val.node.type != T_String) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("only string constants supported in JSON_TABLE path specification"), + parser_errposition(pstate, + exprLocation(jt->common->pathspec)))); + + rootPath = castNode(A_Const, jt->common->pathspec)->val.sval.sval; + + tf->plan = (Node *) transformJsonTableColumns(&cxt, plan, jt->columns, + rootPath, &rootPathName, + jt->common->location); + + /* Also save a copy of the PASSING arguments in the TableFunc node. */ + je = (JsonExpr *) tf->docexpr; + tf->passingvalexprs = copyObject(je->passing_values); + + tf->ordinalitycol = -1; /* undefine ordinality column number */ + tf->location = jt->location; + + pstate->p_lateral_active = false; + + /* + * Mark the RTE as LATERAL if the user said LATERAL explicitly, or if + * there are any lateral cross-references in it. + */ + is_lateral = jt->lateral || contain_vars_of_level((Node *) tf, 0); + + return addRangeTableEntryForTableFunc(pstate, + tf, jt->alias, is_lateral, true); +} diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 41d60494b9..7da42c4772 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -2072,7 +2072,8 @@ addRangeTableEntryForTableFunc(ParseState *pstate, Assert(list_length(tf->coltypmods) == list_length(tf->colnames)); Assert(list_length(tf->colcollations) == list_length(tf->colnames)); - refname = alias ? alias->aliasname : pstrdup("xmltable"); + refname = alias ? alias->aliasname : + pstrdup(tf->functype == TFT_XMLTABLE ? "xmltable" : "json_table"); rte->rtekind = RTE_TABLEFUNC; rte->relid = InvalidOid; @@ -2095,7 +2096,7 @@ addRangeTableEntryForTableFunc(ParseState *pstate, ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), errmsg("%s function has %d columns available but %d columns specified", - "XMLTABLE", + tf->functype == TFT_XMLTABLE ? "XMLTABLE" : "JSON_TABLE", list_length(tf->colnames), numaliases))); rte->eref = eref; diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4f94fc69d6..6500e42485 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1992,6 +1992,9 @@ FigureColnameInternal(Node *node, char **name) case JSON_EXISTS_OP: *name = "json_exists"; return 2; + case JSON_TABLE_OP: + *name = "json_table"; + return 2; } break; default: diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index eded22e194..92d76d5cde 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -61,10 +61,12 @@ #include "catalog/pg_collation.h" #include "catalog/pg_type.h" +#include "executor/execExpr.h" #include "funcapi.h" #include "lib/stringinfo.h" #include "miscadmin.h" #include "nodes/miscnodes.h" +#include "nodes/nodeFuncs.h" #include "regex/regex.h" #include "utils/builtins.h" #include "utils/date.h" @@ -75,6 +77,8 @@ #include "utils/guc.h" #include "utils/json.h" #include "utils/jsonpath.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" #include "utils/timestamp.h" #include "utils/varlena.h" @@ -156,6 +160,61 @@ typedef struct JsonValueListIterator ListCell *next; } JsonValueListIterator; +/* Structures for JSON_TABLE execution */ + +typedef enum JsonTablePlanStateType +{ + JSON_TABLE_SCAN_STATE = 0, + JSON_TABLE_JOIN_STATE +} JsonTablePlanStateType; + +typedef struct JsonTablePlanState +{ + JsonTablePlanStateType type; + + struct JsonTablePlanState *parent; + struct JsonTablePlanState *nested; +} JsonTablePlanState; + +typedef struct JsonTableScanState +{ + JsonTablePlanState plan; + + MemoryContext mcxt; + JsonPath *path; + List *args; + JsonValueList found; + JsonValueListIterator iter; + Datum current; + int ordinal; + bool currentIsNull; + bool outerJoin; + bool errorOnError; + bool advanceNested; + bool reset; +} JsonTableScanState; + +typedef struct JsonTableJoinState +{ + JsonTablePlanState plan; + + JsonTablePlanState *left; + JsonTablePlanState *right; + bool cross; + bool advanceRight; +} JsonTableJoinState; + +/* random number to identify JsonTableExecContext */ +#define JSON_TABLE_EXEC_CONTEXT_MAGIC 418352867 + +typedef struct JsonTableExecContext +{ + int magic; + JsonTableScanState **colexprscans; + JsonTableScanState *root; + bool empty; +} JsonTableExecContext; + /* strict/lax flags is decomposed into four [un]wrap/error flags */ #define jspStrictAbsenseOfErrors(cxt) (!(cxt)->laxMode) #define jspAutoUnwrap(cxt) ((cxt)->laxMode) @@ -248,6 +307,7 @@ static JsonPathExecResult getArrayIndex(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbValue *jb, int32 *index); static JsonBaseObjectInfo setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id); +static void JsonValueListClear(JsonValueList *jvl); static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv); static int JsonValueListLength(const JsonValueList *jvl); static bool JsonValueListIsEmpty(JsonValueList *jvl); @@ -265,6 +325,14 @@ static JsonbValue *wrapItemsInArray(const JsonValueList *items); static int compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, bool useTz, bool *cast_error); + +static JsonTablePlanState *JsonTableInitPlanState(JsonTableExecContext *cxt, + Node *plan, + JsonTablePlanState *parent); +static bool JsonTablePlanNextRow(JsonTablePlanState *state); +static bool JsonTableScanNextRow(JsonTableScanState *scan); + + /****************** User interface to JsonPath executor ********************/ /* @@ -2518,6 +2586,13 @@ setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id) return baseObject; } +static void +JsonValueListClear(JsonValueList *jvl) +{ + jvl->singleton = NULL; + jvl->list = NULL; +} + static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv) { @@ -3123,3 +3198,471 @@ JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); } } + +/************************ JSON_TABLE functions ***************************/ + +/* + * Returns private data from executor state. Ensure validity by check with + * MAGIC number. + */ +static inline JsonTableExecContext * +GetJsonTableExecContext(TableFuncScanState *state, const char *fname) +{ + JsonTableExecContext *result; + + if (!IsA(state, TableFuncScanState)) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + result = (JsonTableExecContext *) state->opaque; + if (result->magic != JSON_TABLE_EXEC_CONTEXT_MAGIC) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + + return result; +} + +/* Recursively initialize JSON_TABLE scan / join state */ +static JsonTableJoinState * +JsonTableInitJoinState(JsonTableExecContext *cxt, JsonTableSibling *plan, + JsonTablePlanState *parent) +{ + JsonTableJoinState *join = palloc0(sizeof(*join)); + + join->plan.type = JSON_TABLE_JOIN_STATE; + /* parent and nested not set. */ + + join->cross = plan->cross; + join->left = JsonTableInitPlanState(cxt, plan->larg, parent); + join->right = JsonTableInitPlanState(cxt, plan->rarg, parent); + + return join; +} + +static JsonTableScanState * +JsonTableInitScanState(JsonTableExecContext *cxt, JsonTableParent *plan, + JsonTablePlanState *parent, + List *args, MemoryContext mcxt) +{ + JsonTableScanState *scan = palloc0(sizeof(*scan)); + int i; + + scan->plan.type = JSON_TABLE_SCAN_STATE; + scan->plan.parent = parent; + + scan->outerJoin = plan->outerJoin; + scan->errorOnError = plan->errorOnError; + scan->path = DatumGetJsonPathP(plan->path->value->constvalue); + scan->args = args; + scan->mcxt = AllocSetContextCreate(mcxt, "JsonTableExecContext", + ALLOCSET_DEFAULT_SIZES); + + /* + * Set after settings scan->args and scan->mcxt, because the recursive call + * wants to use those values. + */ + scan->plan.nested = plan->child ? + JsonTableInitPlanState(cxt, plan->child, (JsonTablePlanState *) scan) : + NULL; + + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + + for (i = plan->colMin; i <= plan->colMax; i++) + cxt->colexprscans[i] = scan; + + return scan; +} + +/* Recursively initialize JSON_TABLE scan state */ +static JsonTablePlanState * +JsonTableInitPlanState(JsonTableExecContext *cxt, Node *plan, + JsonTablePlanState *parent) +{ + JsonTablePlanState *state; + + if (IsA(plan, JsonTableSibling)) + { + JsonTableSibling *join = castNode(JsonTableSibling, plan); + + state = (JsonTablePlanState *) + JsonTableInitJoinState(cxt, join, parent); + } + else + { + JsonTableParent *scan = castNode(JsonTableParent, plan); + JsonTableScanState *parent_scan = (JsonTableScanState *) parent; + + Assert(parent_scan); + state = (JsonTablePlanState *) + JsonTableInitScanState(cxt, scan, parent, parent_scan->args, + parent_scan->mcxt); + } + + return state; +} + +/* + * JsonTableInitOpaque + * Fill in TableFuncScanState->opaque for JsonTable processor + */ +static void +JsonTableInitOpaque(TableFuncScanState *state, int natts) +{ + JsonTableExecContext *cxt; + PlanState *ps = &state->ss.ps; + TableFuncScan *tfs = castNode(TableFuncScan, ps->plan); + TableFunc *tf = tfs->tablefunc; + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + JsonExpr *je = castNode(JsonExpr, tf->docexpr); + List *args = NIL; + + cxt = palloc0(sizeof(JsonTableExecContext)); + cxt->magic = JSON_TABLE_EXEC_CONTEXT_MAGIC; + + if (state->passingvalexprs) + { + ListCell *exprlc; + ListCell *namelc; + + Assert(list_length(state->passingvalexprs) == + list_length(je->passing_names)); + forboth(exprlc, state->passingvalexprs, + namelc, je->passing_names) + { + ExprState *state = lfirst_node(ExprState, exprlc); + String *name = lfirst_node(String, namelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(name->sval); + var->typid = exprType((Node *) state->expr); + var->typmod = exprTypmod((Node *) state->expr); + + /* + * Evaluate the expression and save the value to be returned by + * GetJsonPathVar(). + */ + var->value = ExecEvalExpr(state, ps->ps_ExprContext, + &var->isnull); + + args = lappend(args, var); + } + } + + cxt->colexprscans = palloc(sizeof(JsonTableScanState *) * + list_length(tf->colvalexprs)); + + cxt->root = JsonTableInitScanState(cxt, root, NULL, args, + CurrentMemoryContext); + state->opaque = cxt; +} + +/* Reset scan iterator to the beginning of the item list */ +static void +JsonTableRescan(JsonTableScanState *scan) +{ + JsonValueListInitIterator(&scan->found, &scan->iter); + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + scan->advanceNested = false; + scan->ordinal = 0; +} + +/* Reset context item of a scan, execute JSON path and reset a scan */ +static void +JsonTableResetContextItem(JsonTableScanState *scan, Datum item) +{ + MemoryContext oldcxt; + JsonPathExecResult res; + Jsonb *js = (Jsonb *) DatumGetJsonbP(item); + + JsonValueListClear(&scan->found); + + MemoryContextResetOnly(scan->mcxt); + + oldcxt = MemoryContextSwitchTo(scan->mcxt); + + res = executeJsonPath(scan->path, scan->args, GetJsonPathVar, js, + scan->errorOnError, &scan->found, + false /* FIXME */ ); + + MemoryContextSwitchTo(oldcxt); + + if (jperIsError(res)) + { + Assert(!scan->errorOnError); + JsonValueListClear(&scan->found); /* EMPTY ON ERROR case */ + } + + JsonTableRescan(scan); +} + +/* + * JsonTableSetDocument + * Install the input document + */ +static void +JsonTableSetDocument(TableFuncScanState *state, Datum value) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableSetDocument"); + + JsonTableResetContextItem(cxt->root, value); +} + +/* Recursively reset scan and its child nodes */ +static void +JsonTableRescanRecursive(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTableRescanRecursive(join->left); + JsonTableRescanRecursive(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan = (JsonTableScanState *) state; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + JsonTableRescan(scan); + if (scan->plan.nested) + JsonTableRescanRecursive(scan->plan.nested); + } +} + +/* + * Fetch next row from a cross/union joined scan. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTablePlanNextRow(JsonTablePlanState *state) +{ + JsonTableJoinState *join; + + if (state->type == JSON_TABLE_SCAN_STATE) + return JsonTableScanNextRow((JsonTableScanState *) state); + + join = (JsonTableJoinState *) state; + if (join->advanceRight) + { + /* fetch next inner row */ + if (JsonTablePlanNextRow(join->right)) + return true; + + /* inner rows are exhausted */ + if (join->cross) + join->advanceRight = false; /* next outer row */ + else + return false; /* end of scan */ + } + + while (!join->advanceRight) + { + /* fetch next outer row */ + bool left = JsonTablePlanNextRow(join->left); + + if (join->cross) + { + if (!left) + return false; /* end of scan */ + + JsonTableRescanRecursive(join->right); + + if (!JsonTablePlanNextRow(join->right)) + continue; /* next outer row */ + + join->advanceRight = true; /* next inner row */ + } + else if (!left) + { + if (!JsonTablePlanNextRow(join->right)) + return false; /* end of scan */ + + join->advanceRight = true; /* next inner row */ + } + + break; + } + + return true; +} + +/* Recursively set 'reset' flag of scan and its child nodes */ +static void +JsonTablePlanReset(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTablePlanReset(join->left); + JsonTablePlanReset(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + scan = (JsonTableScanState *) state; + scan->reset = true; + scan->advanceNested = false; + + if (scan->plan.nested) + JsonTablePlanReset(scan->plan.nested); + } +} + +/* + * Fetch next row from a simple scan with outer/inner joined nested subscans. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTableScanNextRow(JsonTableScanState *scan) +{ + /* reset context item if requested */ + if (scan->reset) + { + JsonTableScanState *parent_scan = + (JsonTableScanState *) scan->plan.parent; + + Assert(parent_scan && !parent_scan->currentIsNull); + JsonTableResetContextItem(scan, parent_scan->current); + scan->reset = false; + } + + if (scan->advanceNested) + { + /* fetch next nested row */ + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested) + return true; + } + + for (;;) + { + /* fetch next row */ + JsonbValue *jbv = JsonValueListNext(&scan->found, &scan->iter); + MemoryContext oldcxt; + + if (!jbv) + { + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + return false; /* end of scan */ + } + + /* set current row item */ + oldcxt = MemoryContextSwitchTo(scan->mcxt); + scan->current = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + scan->currentIsNull = false; + MemoryContextSwitchTo(oldcxt); + + scan->ordinal++; + + if (!scan->plan.nested) + break; + + JsonTablePlanReset(scan->plan.nested); + + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested || scan->outerJoin) + break; + } + + return true; +} + +/* + * JsonTableFetchRow + * Prepare the next "current" tuple for upcoming GetValue calls. + * Returns FALSE if the row-filter expression returned no more rows. + */ +static bool +JsonTableFetchRow(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableFetchRow"); + + if (cxt->empty) + return false; + + return JsonTableScanNextRow(cxt->root); +} + +/* + * JsonTableGetValue + * Return the value for column number 'colnum' for the current row. + * + * This leaks memory, so be sure to reset often the context in which it's + * called. + */ +static Datum +JsonTableGetValue(TableFuncScanState *state, int colnum, + Oid typid, int32 typmod, bool *isnull) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableGetValue"); + ExprContext *econtext = state->ss.ps.ps_ExprContext; + ExprState *estate = list_nth(state->colvalexprs, colnum); + JsonTableScanState *scan = cxt->colexprscans[colnum]; + Datum result; + + if (scan->currentIsNull) /* NULL from outer/union join */ + { + result = (Datum) 0; + *isnull = true; + } + else if (estate) /* regular column */ + { + Datum saved_caseValue = econtext->caseValue_datum; + bool saved_caseIsNull = econtext->caseValue_isNull; + + /* Pass the value for CaseTestExpr that may be present in colexpr */ + econtext->caseValue_datum = scan->current; + econtext->caseValue_isNull = false; + + result = ExecEvalExpr(estate, econtext, isnull); + + econtext->caseValue_datum = saved_caseValue; + econtext->caseValue_isNull = saved_caseIsNull; + } + else + { + result = Int32GetDatum(scan->ordinal); /* ordinality column */ + *isnull = false; + } + + return result; +} + +/* + * JsonTableDestroyOpaque + */ +static void +JsonTableDestroyOpaque(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableDestroyOpaque"); + + /* not valid anymore */ + cxt->magic = 0; + + state->opaque = NULL; +} + +const TableFuncRoutine JsonbTableRoutine = +{ + JsonTableInitOpaque, + JsonTableSetDocument, + NULL, + NULL, + NULL, + JsonTableFetchRow, + JsonTableGetValue, + JsonTableDestroyOpaque +}; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d1ec418ccf..e89d1e03d6 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -522,6 +522,8 @@ static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); static void get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit); +static void get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8606,7 +8608,8 @@ get_json_behavior(JsonBehavior *behavior, deparse_context *context, /* * get_json_expr_options * - * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS and + * JSON_TABLE columns. */ static void get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, @@ -9854,6 +9857,9 @@ get_rule_expr(Node *node, deparse_context *context, case JSON_EXISTS_OP: appendStringInfoString(buf, "JSON_EXISTS("); break; + default: + elog(ERROR, "unexpected JsonExpr type: %d", jexpr->op); + break; } get_rule_expr(jexpr->formatted_expr, context, showimplicit); @@ -11207,16 +11213,14 @@ get_sublink_expr(SubLink *sublink, deparse_context *context) /* ---------- - * get_tablefunc - Parse back a table function + * get_xmltable - Parse back a XMLTABLE function * ---------- */ static void -get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +get_xmltable(TableFunc *tf, deparse_context *context, bool showimplicit) { StringInfo buf = context->buf; - /* XMLTABLE is the only existing implementation. */ - appendStringInfoString(buf, "XMLTABLE("); if (tf->ns_uris != NIL) @@ -11307,6 +11311,271 @@ get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) appendStringInfoChar(buf, ')'); } +/* + * get_json_nested_columns - Parse back nested JSON_TABLE columns + */ +static void +get_json_table_nested_columns(TableFunc *tf, Node *node, + deparse_context *context, bool showimplicit, + bool needcomma) +{ + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_nested_columns(tf, n->larg, context, showimplicit, + needcomma); + get_json_table_nested_columns(tf, n->rarg, context, showimplicit, true); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + if (needcomma) + appendStringInfoChar(context->buf, ','); + + appendStringInfoChar(context->buf, ' '); + appendContextKeyword(context, "NESTED PATH ", 0, 0, 0); + get_const_expr(n->path->value, context, -1); + appendStringInfo(context->buf, " AS %s", quote_identifier(n->path->name)); + get_json_table_columns(tf, n, context, showimplicit); + } +} + +/* + * get_json_table_plan - Parse back a JSON_TABLE plan + */ +static void +get_json_table_plan(TableFunc *tf, Node *node, deparse_context *context, + bool parenthesize) +{ + if (parenthesize) + appendStringInfoChar(context->buf, '('); + + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_plan(tf, n->larg, context, + IsA(n->larg, JsonTableSibling) || + castNode(JsonTableParent, n->larg)->child); + + appendStringInfoString(context->buf, n->cross ? " CROSS " : " UNION "); + + get_json_table_plan(tf, n->rarg, context, + IsA(n->rarg, JsonTableSibling) || + castNode(JsonTableParent, n->rarg)->child); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + appendStringInfoString(context->buf, quote_identifier(n->path->name)); + + if (n->child) + { + appendStringInfoString(context->buf, + n->outerJoin ? " OUTER " : " INNER "); + get_json_table_plan(tf, n->child, context, + IsA(n->child, JsonTableSibling)); + } + } + + if (parenthesize) + appendStringInfoChar(context->buf, ')'); +} + +/* + * get_json_table_columns - Parse back JSON_TABLE columns + */ +static void +get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + ListCell *lc_colname; + ListCell *lc_coltype; + ListCell *lc_coltypmod; + ListCell *lc_colvalexpr; + int colnum = 0; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "COLUMNS (", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forfour(lc_colname, tf->colnames, + lc_coltype, tf->coltypes, + lc_coltypmod, tf->coltypmods, + lc_colvalexpr, tf->colvalexprs) + { + char *colname = strVal(lfirst(lc_colname)); + JsonExpr *colexpr; + Oid typid; + int32 typmod; + bool ordinality; + JsonBehaviorType default_behavior; + + typid = lfirst_oid(lc_coltype); + typmod = lfirst_int(lc_coltypmod); + colexpr = castNode(JsonExpr, lfirst(lc_colvalexpr)); + + if (colnum < node->colMin) + { + colnum++; + continue; + } + + if (colnum > node->colMax) + break; + + if (colnum > node->colMin) + appendStringInfoString(buf, ", "); + + colnum++; + + ordinality = !colexpr; + + appendContextKeyword(context, "", 0, 0, 0); + + appendStringInfo(buf, "%s %s", quote_identifier(colname), + ordinality ? "FOR ORDINALITY" : + format_type_with_typemod(typid, typmod)); + if (ordinality) + continue; + + if (colexpr->op == JSON_EXISTS_OP) + { + appendStringInfoString(buf, " EXISTS"); + default_behavior = JSON_BEHAVIOR_FALSE; + } + else + { + if (colexpr->op == JSON_QUERY_OP) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(typid, &typcategory, &typispreferred); + + if (typcategory == TYPCATEGORY_STRING) + appendStringInfoString(buf, + colexpr->format->format_type == JS_FORMAT_JSONB ? + " FORMAT JSONB" : " FORMAT JSON"); + } + + default_behavior = JSON_BEHAVIOR_NULL; + } + + if (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR) + default_behavior = JSON_BEHAVIOR_ERROR; + + appendStringInfoString(buf, " PATH "); + + get_json_path_spec(colexpr->path_spec, context, showimplicit); + + get_json_expr_options(colexpr, context, default_behavior); + } + + if (node->child) + get_json_table_nested_columns(tf, node->child, context, showimplicit, + node->colMax >= node->colMin); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_json_table - Parse back a JSON_TABLE function + * ---------- + */ +static void +get_json_table(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + + appendStringInfoString(buf, "JSON_TABLE("); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_const_expr(root->path->value, context, -1); + + appendStringInfo(buf, " AS %s", quote_identifier(root->path->name)); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PASSING ", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr((Node *) lfirst(lc2), context, false); + appendStringInfo(buf, " AS %s", + quote_identifier((lfirst_node(String, lc1))->sval) + ); + } + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + } + + get_json_table_columns(tf, root, context, showimplicit); + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PLAN ", 0, 0, 0); + get_json_table_plan(tf, (Node *) root, context, true); + + if (jexpr->on_error->btype != JSON_BEHAVIOR_EMPTY) + get_json_behavior(jexpr->on_error, context, "ERROR"); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_tablefunc - Parse back a table function + * ---------- + */ +static void +get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + /* XMLTABLE and JSON_TABLE are the only existing implementations. */ + + if (tf->functype == TFT_XMLTABLE) + get_xmltable(tf, context, showimplicit); + else if (tf->functype == TFT_JSON_TABLE) + get_json_table(tf, context, showimplicit); +} + /* ---------- * get_from_clause - Parse back a FROM clause * diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 584bf7001a..91453681e3 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1879,6 +1879,8 @@ typedef struct TableFuncScanState ExprState *rowexpr; /* state for row-generating expression */ List *colexprs; /* state for column-generating expression */ List *coldefexprs; /* state for column default expressions */ + List *colvalexprs; /* state for column value expression */ + List *passingvalexprs; /* state for PASSING argument expression */ List *ns_names; /* same as TableFunc.ns_names */ List *ns_uris; /* list of states of namespace URI exprs */ Bitmapset *notnulls; /* nullability flag for each output column */ diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 5e149b0266..d8466a2699 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -112,6 +112,8 @@ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *expr, JsonFormat *format); extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); +extern Node *makeJsonTableJoinedPlan(JsonTablePlanJoinType type, + Node *plan1, Node *plan2, int location); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index d4d8445312..b7fc5da28c 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1732,6 +1732,19 @@ typedef enum JsonQuotes */ typedef char *JsonPathSpec; +/* + * JsonTableColumnType - + * enumeration of JSON_TABLE column types + */ +typedef enum JsonTableColumnType +{ + JTC_FOR_ORDINALITY, + JTC_REGULAR, + JTC_EXISTS, + JTC_FORMATTED, + JTC_NESTED, +} JsonTableColumnType; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1785,6 +1798,83 @@ typedef struct JsonFuncExpr int location; /* token location, or -1 if unknown */ } JsonFuncExpr; +/* + * JsonTableColumn - + * untransformed representation of JSON_TABLE column + */ +typedef struct JsonTableColumn +{ + NodeTag type; + JsonTableColumnType coltype; /* column type */ + char *name; /* column name */ + TypeName *typeName; /* column type name */ + char *pathspec; /* path specification, if any */ + char *pathname; /* path name, if any */ + JsonFormat *format; /* JSON format clause, if specified */ + JsonWrapper wrapper; /* WRAPPER behavior for formatted columns */ + bool omit_quotes; /* omit or keep quotes on scalar strings? */ + List *columns; /* nested columns */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + int location; /* token location, or -1 if unknown */ +} JsonTableColumn; + +/* + * JsonTablePlanType - + * flags for JSON_TABLE plan node types representation + */ +typedef enum JsonTablePlanType +{ + JSTP_DEFAULT, + JSTP_SIMPLE, + JSTP_JOINED, +} JsonTablePlanType; + +/* + * JsonTablePlanJoinType - + * flags for JSON_TABLE join types representation + */ +typedef enum JsonTablePlanJoinType +{ + JSTPJ_INNER = 0x01, + JSTPJ_OUTER = 0x02, + JSTPJ_CROSS = 0x04, + JSTPJ_UNION = 0x08, +} JsonTablePlanJoinType; + +typedef struct JsonTablePlan JsonTablePlan; + +/* + * JsonTablePlan - + * untransformed representation of JSON_TABLE plan node + */ +struct JsonTablePlan +{ + NodeTag type; + JsonTablePlanType plan_type; /* plan type */ + JsonTablePlanJoinType join_type; /* join type (for joined plan only) */ + JsonTablePlan *plan1; /* first joined plan */ + JsonTablePlan *plan2; /* second joined plan */ + char *pathname; /* path name (for simple plan only) */ + int location; /* token location, or -1 if unknown */ +}; + +/* + * JsonTable - + * untransformed representation of JSON_TABLE + */ +typedef struct JsonTable +{ + NodeTag type; + JsonCommon *common; /* common JSON path syntax fields */ + List *columns; /* list of JsonTableColumn */ + JsonTablePlan *plan; /* join plan, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + Alias *alias; /* table alias in FROM clause */ + bool lateral; /* does it have LATERAL prefix? */ + int location; /* token location, or -1 if unknown */ +} JsonTable; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 0e5a160c90..926ab12b9e 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -86,8 +86,14 @@ typedef struct RangeVar int location; } RangeVar; +typedef enum TableFuncType +{ + TFT_XMLTABLE, + TFT_JSON_TABLE +} TableFuncType; + /* - * TableFunc - node for a table function, such as XMLTABLE. + * TableFunc - node for a table function, such as XMLTABLE or JSON_TABLE. * * Entries in the ns_names list are either String nodes containing * literal namespace names, or NULL pointers to represent DEFAULT. @@ -95,6 +101,8 @@ typedef struct RangeVar typedef struct TableFunc { NodeTag type; + /* XMLTABLE or JSON_TABLE */ + TableFuncType functype; /* list of namespace URI expressions */ List *ns_uris pg_node_attr(query_jumble_ignore); /* list of namespace names or NULL */ @@ -115,8 +123,14 @@ typedef struct TableFunc List *colexprs; /* list of column default expressions */ List *coldefexprs pg_node_attr(query_jumble_ignore); + /* list of column value expressions */ + List *colvalexprs pg_node_attr(query_jumble_ignore); + /* list of PASSING argument expressions */ + List *passingvalexprs pg_node_attr(query_jumble_ignore); /* nullability flag for each output column */ Bitmapset *notnulls pg_node_attr(query_jumble_ignore); + /* JSON_TABLE plan */ + Node *plan pg_node_attr(query_jumble_ignore); /* counts from 0; -1 if none specified */ int ordinalitycol pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ @@ -1550,7 +1564,8 @@ typedef enum JsonExprOp { JSON_VALUE_OP, /* JSON_VALUE() */ JSON_QUERY_OP, /* JSON_QUERY() */ - JSON_EXISTS_OP /* JSON_EXISTS() */ + JSON_EXISTS_OP, /* JSON_EXISTS() */ + JSON_TABLE_OP /* JSON_TABLE() */ } JsonExprOp; /* @@ -1765,6 +1780,48 @@ typedef struct JsonExpr int location; /* token location, or -1 if unknown */ } JsonExpr; +/* + * JsonTablePath + * A JSON path expression to be computed as part of evaluating + * a JSON_TABLE plan node + */ +typedef struct JsonTablePath +{ + NodeTag type; + + Const *value; + char *name; +} JsonTablePath; + +/* + * JsonTableParent - + * transformed representation of parent JSON_TABLE plan node + */ +typedef struct JsonTableParent +{ + NodeTag type; + JsonTablePath *path; + Node *child; /* nested columns, if any */ + bool outerJoin; /* outer or inner join for nested columns? */ + int colMin; /* min column index in the resulting column + * list */ + int colMax; /* max column index in the resulting column + * list */ + bool errorOnError; /* ERROR/EMPTY ON ERROR behavior */ +} JsonTableParent; + +/* + * JsonTableSibling - + * transformed representation of joined sibling JSON_TABLE plan node + */ +typedef struct JsonTableSibling +{ + NodeTag type; + Node *larg; /* left join node */ + Node *rarg; /* right join node */ + bool cross; /* cross or union join? */ +} JsonTableSibling; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0954d9fc7b..f88a6c9ac6 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -242,6 +242,7 @@ PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_table", JSON_TABLE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) @@ -284,6 +285,7 @@ PG_KEYWORD("names", NAMES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("national", NATIONAL, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("natural", NATURAL, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("nchar", NCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("nested", NESTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("new", NEW, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("next", NEXT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("nfc", NFC, UNRESERVED_KEYWORD, BARE_LABEL) @@ -334,7 +336,9 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h index 8e26482db4..7152a8ef48 100644 --- a/src/include/parser/parse_clause.h +++ b/src/include/parser/parse_clause.h @@ -51,4 +51,7 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle, extern Index assignSortGroupRef(TargetEntry *tle, List *tlist); extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList); +/* functions in parse_jsontable.c */ +extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt); + #endif /* PARSE_CLAUSE_H */ diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index 4dae78a98c..b7ada77cb1 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -15,6 +15,7 @@ #define JSONPATH_H #include "fmgr.h" +#include "executor/tablefunc.h" #include "nodes/pg_list.h" #include "nodes/primnodes.h" #include "utils/jsonb.h" @@ -288,4 +289,6 @@ extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars); +extern PGDLLIMPORT const TableFuncRoutine JsonbTableRoutine; + #endif diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out index 8b87580752..995f267404 100644 --- a/src/test/regress/expected/json_sqljson.out +++ b/src/test/regress/expected/json_sqljson.out @@ -16,3 +16,9 @@ ERROR: JSON_QUERY() is not yet implemented for the json type LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); ^ HINT: Try casting the argument to jsonb +-- JSON_TABLE +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); +ERROR: JSON_TABLE() is not yet implemented for the json type +LINE 1: SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo ... + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out index 7eb8faf487..b235291ec9 100644 --- a/src/test/regress/expected/jsonb_sqljson.out +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -1032,3 +1032,1072 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.dateti ERROR: functions in index expression must be marked IMMUTABLE CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; +-- JSON_TABLE +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); +ERROR: syntax error at or near "(" +LINE 1: SELECT JSON_TABLE('[]', '$'); + ^ +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); +ERROR: syntax error at or near ")" +LINE 1: SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + ^ +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); +ERROR: JSON_TABLE function has 1 columns available but 2 columns specified +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + foo +----- +(0 rows) + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + item | foo +------+----- + 123 | +(1 row) + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + js | id | id2 | int | text | char(4) | bool | numeric | domain | js | jb | jst | jsc | jsv | jsb | jsbq | aaa | aaa1 | exists1 | exists2 | exists3 | exists4 | js2 | jsb2w | jsb2q | ia | ta | jba +---------------------------------------------------------------------------------------+----+-----+-----+---------+---------+------+---------+---------+--------------+--------------+--------------+------+------+--------------+--------------+-----+------+---------+---------+---------+---------+--------------+----------------+--------------+----+----+----- + 1 | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [] | | | | | | | | | | | | | | | | | | | | | | | | | | | + {} | 1 | 1 | | | | | | | {} | {} | {} | {} | {} | {} | {} | | | f | 0 | | false | {} | [{}] | {} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 2 | 2 | 1 | 1.23 | 1.23 | | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | | | f | 0 | | false | 1.23 | [1.23] | 1.23 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 3 | 3 | 2 | 2 | 2 | | 2 | 2 | "2" | "2" | "2" | "2" | "2" | "2" | 2 | | | f | 0 | | false | "2" | ["2"] | 2 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 4 | 4 | | aaaaaaa | aaaa | | | aaaaaaa | "aaaaaaa" | "aaaaaaa" | "aaaaaaa" | "aaa | "aaa | "aaaaaaa" | | | | f | 0 | | false | "aaaaaaa" | ["aaaaaaa"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 5 | 5 | | foo | foo | | | | "foo" | "foo" | "foo" | "foo | "foo | "foo" | | | | f | 0 | | false | "foo" | ["foo"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 6 | 6 | | | | | | | null | null | null | null | null | null | null | | | f | 0 | | false | null | [null] | null | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 7 | 7 | 0 | false | fals | f | | false | false | false | false | fals | fals | false | false | | | f | 0 | | false | false | [false] | false | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 8 | 8 | 1 | true | true | t | | true | true | true | true | true | true | true | true | | | f | 0 | | false | true | [true] | true | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 9 | 9 | | | | | | | {"aaa": 123} | {"aaa": 123} | {"aaa": 123} | {"aa | {"aa | {"aaa": 123} | {"aaa": 123} | 123 | 123 | t | 1 | 1 | true | {"aaa": 123} | [{"aaa": 123}] | {"aaa": 123} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 10 | 10 | | [1,2] | [1,2 | | | [1,2] | "[1,2]" | "[1,2]" | "[1,2]" | "[1, | "[1, | "[1,2]" | [1, 2] | | | f | 0 | | false | "[1,2]" | ["[1,2]"] | [1, 2] | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 11 | 11 | | "str" | "str | | | "str" | "\"str\"" | "\"str\"" | "\"str\"" | "\"s | "\"s | "\"str\"" | "str" | | | f | 0 | | false | "\"str\"" | ["\"str\""] | "str" | | | +(14 rows) + +-- JSON_TABLE: Test backward parsing +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); +\sv jsonb_table_view +CREATE OR REPLACE VIEW public.jsonb_table_view AS + SELECT id, + id2, + "int", + text, + "char(4)", + bool, + "numeric", + domain, + js, + jb, + jst, + jsc, + jsv, + jsb, + jsbq, + aaa, + aaa1, + exists1, + exists2, + exists3, + js2, + jsb2w, + jsb2q, + ia, + ta, + jba, + a1, + b1, + a11, + a21, + a22 + FROM JSON_TABLE( + 'null'::jsonb, '$[*]' AS json_table_path_1 + PASSING + 1 + 2 AS a, + '"foo"'::json AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, + "int" integer PATH '$', + text text PATH '$', + "char(4)" character(4) PATH '$', + bool boolean PATH '$', + "numeric" numeric PATH '$', + domain jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc character(4) FORMAT JSON PATH '$', + jsv character varying(4) FORMAT JSON PATH '$', + jsb jsonb PATH '$', + jsbq jsonb PATH '$' OMIT QUOTES, + aaa integer PATH '$."aaa"', + aaa1 integer PATH '$."aaa"', + exists1 boolean EXISTS PATH '$."aaa"', + exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia integer[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 + COLUMNS ( + a1 integer PATH '$."a1"', + b1 text PATH '$."b1"', + NESTED PATH '$[*]' AS "p1 1" + COLUMNS ( + a11 text PATH '$."a11"' + ) + ), + NESTED PATH '$[2]' AS p2 + COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" + COLUMNS ( + a21 text PATH '$."a21"' + ), + NESTED PATH '$[*]' AS p22 + COLUMNS ( + a22 text PATH '$."a22"' + ) + ) + ) + PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22)))) + ) +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Table Function Scan on "json_table" + Output: "json_table".id, "json_table".id2, "json_table"."int", "json_table".text, "json_table"."char(4)", "json_table".bool, "json_table"."numeric", "json_table".domain, "json_table".js, "json_table".jb, "json_table".jst, "json_table".jsc, "json_table".jsv, "json_table".jsb, "json_table".jsbq, "json_table".aaa, "json_table".aaa1, "json_table".exists1, "json_table".exists2, "json_table".exists3, "json_table".js2, "json_table".jsb2w, "json_table".jsb2q, "json_table".ia, "json_table".ta, "json_table".jba, "json_table".a1, "json_table".b1, "json_table".a11, "json_table".a21, "json_table".a22 + Table Function Call: JSON_TABLE('null'::jsonb, '$[*]' AS json_table_path_1 PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (id FOR ORDINALITY, id2 FOR ORDINALITY, "int" integer PATH '$', text text PATH '$', "char(4)" character(4) PATH '$', bool boolean PATH '$', "numeric" numeric PATH '$', domain jsonb_test_domain PATH '$', js json PATH '$', jb jsonb PATH '$', jst text FORMAT JSON PATH '$', jsc character(4) FORMAT JSON PATH '$', jsv character varying(4) FORMAT JSON PATH '$', jsb jsonb PATH '$', jsbq jsonb PATH '$' OMIT QUOTES, aaa integer PATH '$."aaa"', aaa1 integer PATH '$."aaa"', exists1 boolean EXISTS PATH '$."aaa"', exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, js2 json PATH '$', jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, jsb2q jsonb PATH '$' OMIT QUOTES, ia integer[] PATH '$', ta text[] PATH '$', jba jsonb[] PATH '$', NESTED PATH '$[1]' AS p1 COLUMNS (a1 integer PATH '$."a1"', b1 text PATH '$."b1"', NESTED PATH '$[*]' AS "p1 1" COLUMNS (a11 text PATH '$."a11"')), NESTED PATH '$[2]' AS p2 COLUMNS ( NESTED PATH '$[*]' AS "p2:1" COLUMNS (a21 text PATH '$."a21"'), NESTED PATH '$[*]' AS p22 COLUMNS (a22 text PATH '$."a22"'))) PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22))))) +(3 rows) + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + js | a +-------+--- + 1 | 1 + "err" | +(2 rows) + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; + a +--- + +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: jsonpath member accessor can only be applied to an object +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: no SQL/JSON item +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 1 +(1 row) + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); + a +--- + 0 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to smallint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to bigint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to real +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 E... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); + a +----- + fal +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to json +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to jsonb +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EX... + ^ +-- JSON_TABLE: nested paths and plans +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb '[]', '$' -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 4: NESTED PATH '$' COLUMNS ( -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: b +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +-- JSON_TABLE: plan validation +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p1) + ^ +DETAIL: Path name mismatch: expected p0 but p1 is given. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 UNION p1 UNION p11) + ^ +DETAIL: Expected INNER or OUTER JSON_TABLE plan node. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 8: NESTED PATH '$' AS p2 COLUMNS ( + ^ +DETAIL: Plan node for nested path p2 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 5: NESTED PATH '$' AS p11 COLUMNS ( foo int ), + ^ +DETAIL: Plan node for nested path p11 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) + ^ +DETAIL: Plan node contains some extra or duplicate sibling nodes. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 6: NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ^ +DETAIL: Plan node for nested path p12 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 9: NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ^ +DETAIL: Plan node for nested path p21 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + bar | foo | baz +-----+-----+----- +(0 rows) + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb 'null', 'strict $[*]' -- without root path name + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- JSON_TABLE: plan execution +CREATE TEMP TABLE jsonb_table_test (js jsonb); +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + n | a | c | b +---+----+----+--- + 1 | 1 | | + 2 | 2 | 10 | + 2 | 2 | | + 2 | 2 | 20 | + 2 | 2 | | 1 + 2 | 2 | | 2 + 2 | 2 | | 3 + 3 | 3 | | 1 + 3 | 3 | | 2 + 4 | -1 | | 1 + 4 | -1 | | 2 +(11 rows) + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + n | a | b | b1 | c | c1 | b +---+---+--------------+-----+------+----+----- + 1 | 1 | [1, 10] | 1 | 1 | | 101 + 1 | 1 | [1, 10] | 1 | null | | 101 + 1 | 1 | [1, 10] | 1 | 2 | | 101 + 1 | 1 | [1, 10] | 10 | 1 | | 110 + 1 | 1 | [1, 10] | 10 | null | | 110 + 1 | 1 | [1, 10] | 10 | 2 | | 110 + 1 | 1 | [2] | 2 | 1 | | 102 + 1 | 1 | [2] | 2 | null | | 102 + 1 | 1 | [2] | 2 | 2 | | 102 + 1 | 1 | [3, 30, 300] | 3 | 1 | | 103 + 1 | 1 | [3, 30, 300] | 3 | null | | 103 + 1 | 1 | [3, 30, 300] | 3 | 2 | | 103 + 1 | 1 | [3, 30, 300] | 30 | 1 | | 130 + 1 | 1 | [3, 30, 300] | 30 | null | | 130 + 1 | 1 | [3, 30, 300] | 30 | 2 | | 130 + 1 | 1 | [3, 30, 300] | 300 | 1 | | 400 + 1 | 1 | [3, 30, 300] | 300 | null | | 400 + 1 | 1 | [3, 30, 300] | 300 | 2 | | 400 + 2 | 2 | | | | | + 3 | | | | | | +(20 rows) + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + x | y | y | z +---+---+--------------+--- + 2 | 1 | [1, 2, 3] | 1 + 2 | 1 | [1, 2, 3] | 2 + 2 | 1 | [1, 2, 3] | 3 + 3 | 1 | [1, 2, 3] | 1 + 3 | 1 | [1, 2, 3] | 2 + 3 | 1 | [1, 2, 3] | 3 + 3 | 1 | [2, 3, 4, 5] | 2 + 3 | 1 | [2, 3, 4, 5] | 3 + 3 | 1 | [2, 3, 4, 5] | 4 + 3 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [1, 2, 3] | 1 + 4 | 1 | [1, 2, 3] | 2 + 4 | 1 | [1, 2, 3] | 3 + 4 | 1 | [2, 3, 4, 5] | 2 + 4 | 1 | [2, 3, 4, 5] | 3 + 4 | 1 | [2, 3, 4, 5] | 4 + 4 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [3, 4, 5, 6] | 3 + 4 | 1 | [3, 4, 5, 6] | 4 + 4 | 1 | [3, 4, 5, 6] | 5 + 4 | 1 | [3, 4, 5, 6] | 6 + 2 | 2 | [1, 2, 3] | 2 + 2 | 2 | [1, 2, 3] | 3 + 3 | 2 | [1, 2, 3] | 2 + 3 | 2 | [1, 2, 3] | 3 + 3 | 2 | [2, 3, 4, 5] | 2 + 3 | 2 | [2, 3, 4, 5] | 3 + 3 | 2 | [2, 3, 4, 5] | 4 + 3 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [1, 2, 3] | 2 + 4 | 2 | [1, 2, 3] | 3 + 4 | 2 | [2, 3, 4, 5] | 2 + 4 | 2 | [2, 3, 4, 5] | 3 + 4 | 2 | [2, 3, 4, 5] | 4 + 4 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [3, 4, 5, 6] | 3 + 4 | 2 | [3, 4, 5, 6] | 4 + 4 | 2 | [3, 4, 5, 6] | 5 + 4 | 2 | [3, 4, 5, 6] | 6 + 2 | 3 | [1, 2, 3] | 3 + 3 | 3 | [1, 2, 3] | 3 + 3 | 3 | [2, 3, 4, 5] | 3 + 3 | 3 | [2, 3, 4, 5] | 4 + 3 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [1, 2, 3] | 3 + 4 | 3 | [2, 3, 4, 5] | 3 + 4 | 3 | [2, 3, 4, 5] | 4 + 4 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [3, 4, 5, 6] | 3 + 4 | 3 | [3, 4, 5, 6] | 4 + 4 | 3 | [3, 4, 5, 6] | 5 + 4 | 3 | [3, 4, 5, 6] | 6 +(52 rows) + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; +ERROR: could not find jsonpath variable "x" +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); + json_value +------------ + foo +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_query +------------ + 123 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); + json_query +------------ + [123] +(1 row) + +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +ERROR: syntax error at or near " " of jsonpath input +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); +ERROR: only string constants supported in JSON_TABLE path specification +LINE 1: SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || '... + ^ diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql index 4f30fa46b9..df4a430d88 100644 --- a/src/test/regress/sql/json_sqljson.sql +++ b/src/test/regress/sql/json_sqljson.sql @@ -9,3 +9,7 @@ SELECT JSON_VALUE(NULL FORMAT JSON, '$'); -- JSON_QUERY SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + +-- JSON_TABLE + +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql index 963129699b..b8ef02803b 100644 --- a/src/test/regress/sql/jsonb_sqljson.sql +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -323,3 +323,632 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; + +-- JSON_TABLE + +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); + +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); + +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); + +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + +-- JSON_TABLE: Test backward parsing + +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); + +\sv jsonb_table_view + +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; + +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); + +-- JSON_TABLE: nested paths and plans + +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; + +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; + +-- JSON_TABLE: plan validation + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; + +-- JSON_TABLE: plan execution + +CREATE TEMP TABLE jsonb_table_test (js jsonb); + +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); + +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; + +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e284d9db51..38af66526a 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1303,6 +1303,7 @@ JsonPathKeyword JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback +JsonPathSpec JsonPathString JsonPathVarCallback JsonPathVariable @@ -1311,6 +1312,17 @@ JsonQuotes JsonReturning JsonSemAction JsonSerializeExpr +JsonTable +JsonTableColumn +JsonTableColumnType +JsonTableContext +JsonTableJoinState +JsonTableParent +JsonTablePlan +JsonTablePlanJoinType +JsonTablePlanType +JsonTableScanState +JsonTableSibling JsonTokenType JsonTransformStringValuesAction JsonTypeCategory @@ -2766,6 +2778,7 @@ TableFunc TableFuncRoutine TableFuncScan TableFuncScanState +TableFuncType TableInfo TableLikeClause TableSampleClause -- 2.35.3
v1-0002-SQL-JSON-query-functions.patch (application/octet-stream, 208.8K) download From bd975ac592b08c374e4bfc8113e14e215080d0a3 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Fri, 16 Jun 2023 17:49:18 +0900 Subject: [PATCH v1 2/4] SQL/JSON query functions This introduces the SQL/JSON functions for querying JSON data using jsonpath expressions. The functions are: JSON_EXISTS() JSON_QUERY() JSON_VALUE() All of these functions only operate on jsonb. The workaround for now is to cast the argument to jsonb. JSON_EXISTS() tests if the jsonpath expression applied to the jsonb value yields any values. JSON_VALUE() must return a single value, and an error occurs if it tries to return multiple values. JSON_QUERY() must return a json object or array, and there are various WRAPPER options for handling scalar or multi-value results. Both these functions have options for handling EMPTY and ERROR conditions. Nikita Glukhov Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby. Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org --- doc/src/sgml/func.sgml | 147 +++ src/backend/executor/execExpr.c | 432 ++++++++ src/backend/executor/execExprInterp.c | 502 ++++++++- src/backend/jit/llvm/llvmjit_expr.c | 250 +++++ src/backend/jit/llvm/llvmjit_types.c | 5 + src/backend/nodes/makefuncs.c | 15 + src/backend/nodes/nodeFuncs.c | 183 ++++ src/backend/optimizer/path/costsize.c | 3 +- src/backend/optimizer/util/clauses.c | 19 + src/backend/parser/gram.y | 329 +++++- src/backend/parser/parse_collate.c | 7 + src/backend/parser/parse_expr.c | 544 +++++++++- src/backend/parser/parse_target.c | 15 + src/backend/utils/adt/formatting.c | 44 + src/backend/utils/adt/jsonb.c | 62 ++ src/backend/utils/adt/jsonfuncs.c | 170 ++- src/backend/utils/adt/jsonpath.c | 255 +++++ src/backend/utils/adt/jsonpath_exec.c | 391 ++++++- src/backend/utils/adt/ruleutils.c | 136 +++ src/include/executor/execExpr.h | 172 +++ src/include/executor/executor.h | 1 + src/include/nodes/execnodes.h | 3 + src/include/nodes/makefuncs.h | 1 + src/include/nodes/parsenodes.h | 48 + src/include/nodes/primnodes.h | 109 ++ src/include/parser/kwlist.h | 11 + src/include/utils/formatting.h | 2 +- src/include/utils/jsonb.h | 3 + src/include/utils/jsonfuncs.h | 6 + src/include/utils/jsonpath.h | 27 + src/interfaces/ecpg/preproc/ecpg.trailer | 28 + src/test/regress/expected/json_sqljson.out | 18 + src/test/regress/expected/jsonb_sqljson.out | 1034 +++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/json_sqljson.sql | 11 + src/test/regress/sql/jsonb_sqljson.sql | 325 ++++++ src/tools/pgindent/typedefs.list | 14 + 37 files changed, 5222 insertions(+), 102 deletions(-) create mode 100644 src/test/regress/expected/json_sqljson.out create mode 100644 src/test/regress/expected/jsonb_sqljson.out create mode 100644 src/test/regress/sql/json_sqljson.sql create mode 100644 src/test/regress/sql/jsonb_sqljson.sql diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 8de8f527a5..e7a018583a 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16995,6 +16995,153 @@ array w/o UK? | t </tbody> </tgroup> </table> + + <para> + <xref linkend="functions-sqljson-querying"/> details the SQL/JSON + functions that can be used to query JSON data. + </para> + + <note> + <para> + SQL/JSON paths can only be applied to the <type>jsonb</type> type, so it + might be necessary to cast the <replaceable>context_item</replaceable> + argument of these functions to <type>jsonb</type>. + </para> + </note> + + <table id="functions-sqljson-querying"> + <title>SQL/JSON Query Functions</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="func_table_entry"><para role="func_signature"> + Function signature + </para> + <para> + Description + </para> + <para> + Example(s) + </para></entry> + </row> + </thead> + <tbody> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_exists</primary></indexterm> + <function>json_exists</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>TRUE</literal> | <literal>FALSE</literal> |<literal> UNKNOWN</literal> | <literal>ERROR</literal> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns true if the SQL/JSON <replaceable>path_expression</replaceable> + applied to the <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s yields any items. + The <literal>ON ERROR</literal> clause specifies what is returned if + an error occurs. Note that if the <replaceable>path_expression</replaceable> + is <literal>strict</literal>, an error is generated if it yields no items. + The default value is <literal>UNKNOWN</literal> which causes a NULL + result. + </para> + <para> + <literal>json_exists(jsonb '{"key1": [1,2,3]}', 'strict $.key1[*] ? (@ > 2)')</literal> + <returnvalue>t</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'lax $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>f</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'strict $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>ERROR: jsonpath array subscript is out of bounds</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_value</primary></indexterm> + <function>json_value</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> + <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. The extracted value must be + a single <acronym>SQL/JSON</acronym> scalar item. For results that + are objects or arrays, use the <function>json_query</function> + function instead. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + The <literal>ON EMPTY</literal> clause specifies the behavior if the + <replaceable>path_expression</replaceable> yields no value at all. + The <literal>ON ERROR</literal> clause specifies the behavior if an + error occurs as a result of <type>jsonpath</type> evaluation + (including cast to the output type) or execution of + <literal>ON EMPTY</literal> behavior (that was caused by empty result + of <type>jsonpath</type> evaluation). + </para> + <para> + <literal>json_value(jsonb '"123.45"', '$' RETURNING float)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_value(jsonb '"03:04 2015-02-01"', '$.datetime("HH24:MI&nbsp;YYYY-MM-DD")' RETURNING date)</literal> + <returnvalue>2015-02-01</returnvalue> + </para> + <para> + <literal>json_value(jsonb '[1,2]', 'strict $[*]' DEFAULT 9 ON ERROR)</literal> + <returnvalue>9</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_query</primary></indexterm> + <function>json_query</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional> + <optional> { <literal>WITHOUT</literal> | <literal>WITH</literal> { <literal>CONDITIONAL</literal> | <optional><literal>UNCONDITIONAL</literal></optional> } } <optional> <literal>ARRAY</literal> </optional> <literal>WRAPPER</literal> </optional> + <optional> { <literal>KEEP</literal> | <literal>OMIT</literal> } <literal>QUOTES</literal> <optional> <literal>ON SCALAR STRING</literal> </optional> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. + This function must return a JSON string, so if the path expression + returns multiple SQL/JSON items, you must wrap the result using the + <literal>WITH WRAPPER</literal> clause. If the wrapper is + <literal>UNCONDITIONAL</literal>, an array wrapper will always + be applied, even if the returned value is already a single JSON object + or array, but if it is <literal>CONDITIONAL</literal>, it will not be + applied to a single array or object. <literal>UNCONDITIONAL</literal> + is the default. + If the result is a scalar string, by default the value returned will have + surrounding quotes making it a valid JSON value. However, this behavior + is reversed if <literal>OMIT QUOTES</literal> is specified. + The <literal>ON ERROR</literal> and <literal>ON EMPTY</literal> + clauses have similar semantics to those clauses for + <function>json_value</function>. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + </para> + <para> + <literal>json_query(jsonb '[1,[2,3],null]', 'lax $[*][1]' WITH CONDITIONAL WRAPPER)</literal> + <returnvalue>[3]</returnvalue> + </para></entry> + </row> + </tbody> + </tgroup> + </table> + </sect2> <sect2 id="functions-sqljson-path"> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index b958ae5985..49d2841047 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -50,6 +50,7 @@ #include "utils/datum.h" #include "utils/json.h" #include "utils/jsonb.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -89,6 +90,16 @@ static void ExecBuildAggTransCall(ExprState *state, AggState *aggstate, FunctionCallInfo fcinfo, AggStatePerTrans pertrans, int transno, int setno, int setoff, bool ishash, bool nullcheck); +static void ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch); +static Datum GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null); +static JsonCoercionState *ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull); +static JsonItemCoercionsState *ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull); /* @@ -2439,6 +2450,14 @@ ExecInitExprRec(Expr *node, ExprState *state, break; } + case T_JsonExpr: + { + JsonExpr *jexpr = castNode(JsonExpr, node); + + ExecInitJsonExpr(jexpr, state, resv, resnull, &scratch); + break; + } + case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -4206,3 +4225,416 @@ ExecBuildParamSetEqual(TupleDesc desc, return state; } + +/* + * Push steps to evaluate a JsonExpr and its various subsidiary expressions. + */ +static void +ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch) +{ + JsonExprState *jsestate = palloc0(sizeof(JsonExprState)); + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + ListCell *argexprlc; + ListCell *argnamelc; + int skip_step_off = -1; + int passing_args_step_off = -1; + int coercion_step_off = -1; + int coercion_finish_step_off = -1; + int behavior_step_off = -1; + int onempty_expr_step_off = -1; + int onempty_jump_step_off = -1; + int onerror_expr_step_off = -1; + int onerror_jump_step_off = -1; + List *adjust_jumps = NIL; + ListCell *lc; + ExprEvalStep *as; + + jsestate->jsexpr = jexpr; + + /* + * Add steps to compute formatted_expr, pathspec, and PASSING arg + * expressions as things that must be evaluated *before* the actual JSON + * path expression. + */ + ExecInitExprRec((Expr *) jexpr->formatted_expr, state, + &pre_eval->formatted_expr.value, + &pre_eval->formatted_expr.isnull); + ExecInitExprRec((Expr *) jexpr->path_spec, state, + &pre_eval->pathspec.value, + &pre_eval->pathspec.isnull); + + /* + * Before pushing steps for PASSING args, push a step to decide whether to + * skip evaluating the args and the JSON path expression depending on + * whether either of formatted_expr and pathspec is NULL; see + * ExecEvalJsonExprSkip(). + */ + scratch->opcode = EEOP_JSONEXPR_SKIP; + scratch->d.jsonexpr_skip.jsestate = jsestate; + skip_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* PASSING args. */ + jsestate->pre_eval.args = NIL; + passing_args_step_off = state->steps_len; + forboth(argexprlc, jexpr->passing_values, + argnamelc, jexpr->passing_names) + { + Expr *argexpr = (Expr *) lfirst(argexprlc); + String *argname = lfirst_node(String, argnamelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(argname->sval); + var->typid = exprType((Node *) argexpr); + var->typmod = exprTypmod((Node *) argexpr); + + ExecInitExprRec((Expr *) argexpr, state, &var->value, &var->isnull); + + pre_eval->args = lappend(pre_eval->args, var); + } + + /* + * Step for the actual JSON path evaluation; see ExecEvalJson() and + * ExecEvalJsonExpr(). + */ + scratch->opcode = EEOP_JSONEXPR_PATH; + scratch->d.jsonexpr.jsestate = jsestate; + ExprEvalPushStep(state, scratch); + + /* + * Push steps to control the evaluation of expressions based on the result + * of JSON path evaluation. + */ + + /* + * Step to handle ON ERROR and ON EMPTY behavior; see + * ExecEvalJsonExprBehavior(). + */ + scratch->opcode = EEOP_JSONEXPR_BEHAVIOR; + scratch->d.jsonexpr_behavior.jsestate = jsestate; + behavior_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Step(s) to evaluate ON EMPTY expression */ + onempty_expr_step_off = state->steps_len; + if (jexpr->on_empty && + jexpr->on_empty->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_empty->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_empty->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_empty, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onempty_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + } + } + + /* Step(s) to evaluate ON ERROR expression */ + onerror_expr_step_off = state->steps_len; + if (jexpr->on_error && + jexpr->on_error->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_error->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_error->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_error, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onerror_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set later */ + ExprEvalPushStep(state, scratch); + } + } + + /* + * Step to handle applying coercion to the JSON item returned by + * EEOP_JSONEXPR_PATH or to the ON EMPTY/ERROR expression as + * EEOP_JSONEXPR_BEHAVIOR decides; see ExecEvalJsonExprCoercion(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Initialize coercion expression(s). */ + if (jexpr->result_coercion) + { + jsestate->result_jcstate = + ExecInitJsonCoercion(scratch, state, jexpr->result_coercion, + resv, resnull); + /* See the comment above. */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* computed later */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + if (jexpr->coercions) + { + /* + * ExecPrepareJsonItemCoercion() called by ExecEvalJsonExpr() chooses + * one for a given JSON item returned by JsonPathValue(). + */ + jsestate->item_jcstates = + ExecInitJsonItemCoercions(scratch, state, jexpr->coercions, + resv, resnull); + } + + /* + * And a step to clean up after the coercion step; see + * ExecEvalJsonExprCoercionFinish(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION_FINISH; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_finish_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* + * Adjust jump target addresses in various post-eval steps now that we + * have all the steps in place. + */ + + /* EEOP_JSONEXPR_SKIP */ + Assert(skip_step_off >= 0); + as = &state->steps[skip_step_off]; + as->d.jsonexpr_skip.jump_coercion = coercion_step_off; + as->d.jsonexpr_skip.jump_passing_args = passing_args_step_off; + + /* EEOP_JSONEXPR_BEHAVIOR */ + Assert(behavior_step_off >= 0); + as = &state->steps[behavior_step_off]; + as->d.jsonexpr_behavior.jump_onerror_expr = onerror_expr_step_off; + as->d.jsonexpr_behavior.jump_onempty_expr = onempty_expr_step_off; + as->d.jsonexpr_behavior.jump_coercion = coercion_step_off; + as->d.jsonexpr_behavior.jump_skip_coercion = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION */ + Assert(coercion_step_off >= 0); + as = &state->steps[coercion_step_off]; + as->d.jsonexpr_coercion.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION_FINISH */ + Assert(coercion_finish_step_off >= 0); + as = &state->steps[coercion_finish_step_off]; + as->d.jsonexpr_coercion_finish.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion_finish.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JUMP steps */ + + /* + * Ones after ON EMPTY and ON ERROR non-default expressions should jump to + * the coercion step. + */ + if (onempty_jump_step_off >= 0) + { + as = &state->steps[onempty_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + if (onerror_jump_step_off >= 0) + { + as = &state->steps[onerror_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + + /* The rest should jump to the end. */ + foreach(lc, adjust_jumps) + { + as = &state->steps[lfirst_int(lc)]; + as->d.jump.jumpdone = state->steps_len; + } + + /* + * Set RETURNING type's input function used by ExecEvalJsonExprCoercion(). + */ + if (jexpr->omit_quotes || + (jexpr->result_coercion && jexpr->result_coercion->via_io)) + { + Oid typinput; + FmgrInfo *finfo; + + /* lookup the result type's input function */ + getTypeInputInfo(jexpr->returning->typid, &typinput, + &jsestate->input.typioparam); + finfo = palloc0(sizeof(FmgrInfo)); + fmgr_info(typinput, finfo); + jsestate->input.finfo = finfo; + } +} + +/* + * Evaluate a JSON error/empty behavior result. + */ +static Datum +GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null) +{ + *is_null = false; + + switch (behavior->btype) + { + case JSON_BEHAVIOR_EMPTY_ARRAY: + return JsonbPGetDatum(JsonbMakeEmptyArray()); + + case JSON_BEHAVIOR_EMPTY_OBJECT: + return JsonbPGetDatum(JsonbMakeEmptyObject()); + + case JSON_BEHAVIOR_TRUE: + return BoolGetDatum(true); + + case JSON_BEHAVIOR_FALSE: + return BoolGetDatum(false); + + case JSON_BEHAVIOR_NULL: + case JSON_BEHAVIOR_UNKNOWN: + case JSON_BEHAVIOR_EMPTY: + *is_null = true; + return (Datum) 0; + + case JSON_BEHAVIOR_DEFAULT: + /* Always handled in the caller. */ + Assert(false); + return (Datum) 0; + + default: + elog(ERROR, "unrecognized SQL/JSON behavior %d", behavior->btype); + return (Datum) 0; + } +} + +/* Initialize one JsonCoercion for execution. */ +static JsonCoercionState * +ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull) +{ + JsonCoercionState *jcstate = palloc0(sizeof(JsonCoercionState)); + + jcstate->coercion = coercion; + if (coercion && coercion->expr) + { + Datum *save_innermost_caseval; + bool *save_innermost_casenull; + + jcstate->jump_eval_expr = state->steps_len; + + /* Push step(s) to compute cstate->coercion. */ + save_innermost_caseval = state->innermost_caseval; + save_innermost_casenull = state->innermost_casenull; + + state->innermost_caseval = resv; + state->innermost_casenull = resnull; + + ExecInitExprRec((Expr *) coercion->expr, state, resv, resnull); + + state->innermost_caseval = save_innermost_caseval; + state->innermost_casenull = save_innermost_casenull; + } + else + jcstate->jump_eval_expr = -1; + + return jcstate; +} + +/* + * Push steps to evaluate coercions from a given JsonItemCoercions, which + * contains all possible coercions that may need to be applied to JSON + * items coming from evaluating the parent JsonExpr. + */ +static JsonItemCoercionsState * +ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull) +{ + JsonCoercion **coercion; + JsonItemCoercionsState *item_jcstates = palloc0(sizeof(JsonItemCoercionsState)); + JsonCoercionState **item_jcstate; + ExprEvalStep *as; + List *adjust_jumps = NIL; + ListCell *lc; + + /* Push the steps of individual coercions. */ + for (coercion = &coercions->null, + item_jcstate = &item_jcstates->null; + coercion <= &coercions->composite; + coercion++, item_jcstate++) + { + *item_jcstate = ExecInitJsonCoercion(scratch, state, *coercion, + resv, resnull); + + /* Emit JUMP step to skip past other coercions' steps. */ + scratch->opcode = EEOP_JUMP; + + /* + * Remember JUMP step address to set the actual jump target address + * below. + */ + adjust_jumps = lappend_int(adjust_jumps, state->steps_len); + ExprEvalPushStep(state, scratch); + } + + foreach(lc, adjust_jumps) + { + int jump_step_id = lfirst_int(lc); + + as = &state->steps[jump_step_id]; + as->d.jump.jumpdone = state->steps_len; + } + + return item_jcstates; +} diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 9a234eb8ba..8d57ac7a92 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -74,6 +74,7 @@ #include "utils/json.h" #include "utils/jsonb.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/timestamp.h" @@ -152,6 +153,9 @@ static TupleDesc get_cached_rowtype(Oid type_id, int32 typmod, bool *changed); static void ExecEvalRowNullInt(ExprState *state, ExprEvalStep *op, ExprContext *econtext, bool checkisnull); +static Datum ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate); /* fast-path evaluation functions */ static Datum ExecJustInnerVar(ExprState *state, ExprContext *econtext, bool *isnull); @@ -480,6 +484,11 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_XMLEXPR, &&CASE_EEOP_JSON_CONSTRUCTOR, &&CASE_EEOP_IS_JSON, + &&CASE_EEOP_JSONEXPR_SKIP, + &&CASE_EEOP_JSONEXPR_PATH, + &&CASE_EEOP_JSONEXPR_BEHAVIOR, + &&CASE_EEOP_JSONEXPR_COERCION, + &&CASE_EEOP_JSONEXPR_COERCION_FINISH, &&CASE_EEOP_AGGREF, &&CASE_EEOP_GROUPING_FUNC, &&CASE_EEOP_WINDOW_FUNC, @@ -1186,8 +1195,14 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) /* second and third arguments are already set up */ fcinfo_in->isnull = false; + + /* Pass down soft-error capture node if any. */ + fcinfo_in->context = state->escontext; + d = FunctionCallInvoke(fcinfo_in); *op->resvalue = d; + if (SOFT_ERROR_OCCURRED(state->escontext)) + *op->resnull = true; /* Should get null result if and only if str is NULL */ if (str == NULL) @@ -1195,7 +1210,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) Assert(*op->resnull); Assert(fcinfo_in->isnull); } - else + else if (!SOFT_ERROR_OCCURRED(state->escontext)) { Assert(!*op->resnull); Assert(!fcinfo_in->isnull); @@ -1543,6 +1558,38 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_JSONEXPR_PATH) + { + /* too complex for an inline implementation */ + ExecEvalJsonExpr(state, op, econtext); + EEO_NEXT(); + } + + EEO_CASE(EEOP_JSONEXPR_SKIP) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprSkip(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_BEHAVIOR) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprBehavior(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercion(state, op, econtext, + *op->resvalue, *op->resnull)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION_FINISH) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercionFinish(state, op)); + } + EEO_CASE(EEOP_AGGREF) { /* @@ -3740,7 +3787,7 @@ ExecEvalConstraintCheck(ExprState *state, ExprEvalStep *op) { if (!*op->d.domaincheck.checknull && !DatumGetBool(*op->d.domaincheck.checkvalue)) - ereport(ERROR, + errsave(state->escontext, (errcode(ERRCODE_CHECK_VIOLATION), errmsg("value for domain %s violates check constraint \"%s\"", format_type_be(op->d.domaincheck.resulttype), @@ -4132,6 +4179,457 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op) *op->resvalue = BoolGetDatum(res); } +/* + * Evaluate given JsonExpr by performing the specified JSON operation. + * + * This also populates the JsonExprPostEvalState with the information needed + * by the subsequent steps that handle the specified JsonBehavior. + */ +void +ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) +{ + JsonExprState *jsestate = op->d.jsonexpr.jsestate; + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonExpr *jexpr = jsestate->jsexpr; + Datum item; + Datum res = (Datum) 0; + bool resnull = true; + JsonPath *path; + bool *error; + bool *empty; + + item = pre_eval->formatted_expr.value; + path = DatumGetJsonPathP(pre_eval->pathspec.value); + + /* Reset JsonExprPostEvalState for this evaluation. */ + memset(post_eval, 0, sizeof(*post_eval)); + + /* Respect ON ERROR behavior during path evaluation. */ + jsestate->throw_error = (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR); + + /* + * Be sure to save the error (if needed) and empty statuses for the + * EEOP_JSONEXPR_BEHAVIOR step to peruse. + */ + error = !jsestate->throw_error ? &post_eval->error : NULL; + empty = &post_eval->empty; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + res = JsonPathQuery(item, path, jexpr->wrapper, empty, error, + pre_eval->args); + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + resnull = !DatumGetPointer(res); + break; + + case JSON_VALUE_OP: + { + JsonbValue *jbv = JsonPathValue(item, path, empty, error, + pre_eval->args); + + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + if (!jbv) /* NULL or empty */ + { + resnull = true; + break; + } + + Assert(!*empty); + + resnull = false; + + /* coerce scalar item to the output type */ + if (jexpr->returning->typid == JSONOID || + jexpr->returning->typid == JSONBOID) + { + /* Use result coercion from json[b] to the output type */ + res = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + break; + } + + /* + * Error out if no cast exists to coerce SQL/JSON item to the + * the output type. + */ + Assert(post_eval->item_jcstate == NULL); + res = ExecPrepareJsonItemCoercion(jbv, + jsestate->item_jcstates, + &post_eval->item_jcstate); + if (post_eval->item_jcstate && + post_eval->item_jcstate->coercion && + (post_eval->item_jcstate->coercion->via_io || + post_eval->item_jcstate->coercion->via_populate)) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * Coercion via I/O means here that the cast to the target + * type simply does not exist. + */ + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE), + errmsg("SQL/JSON item cannot be cast to target type"))); + } + break; + } + + case JSON_EXISTS_OP: + { + bool exists = JsonPathExists(item, path, + pre_eval->args, + error); + + resnull = error && *error; + res = BoolGetDatum(exists); + break; + } + + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * If the ON EMPTY behavior is to cause an error, do so here. Other + * behaviors will be handled by the caller. + */ + if (*empty) + { + Assert(jexpr->on_empty); /* it is not JSON_EXISTS */ + + if (jexpr->on_empty->btype == JSON_BEHAVIOR_ERROR) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + ereport(ERROR, + (errcode(ERRCODE_NO_SQL_JSON_ITEM), + errmsg("no SQL/JSON item"))); + } + } + + *op->resvalue = res; + *op->resnull = resnull; +} + +/* + * Skip calling ExecEvalJson() on the given JsonExpr? + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_skip.jsestate; + + /* + * Skip if either of the input expressions has turned out to be NULL, + * though do execute domain checks for NULLs, which are handled by the + * coercion step. + */ + if (jsestate->pre_eval.formatted_expr.isnull || + jsestate->pre_eval.pathspec.isnull) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_skip.jump_coercion; + } + + /* + * Go evaluate the PASSING args if any and subsequently JSON path itself. + */ + return op->d.jsonexpr_skip.jump_passing_args; +} + +/* + * Returns the step address to perform the JsonBehavior applicable to + * the JSON item that resulted from evaluating the given JsonExpr. + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_behavior.jsestate; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonBehavior *behavior = NULL; + int jump_to = -1; + + if (post_eval->error || post_eval->coercion_error) + { + behavior = jsestate->jsexpr->on_error; + jump_to = op->d.jsonexpr_behavior.jump_onerror_expr; + } + else if (post_eval->empty) + { + behavior = jsestate->jsexpr->on_empty; + jump_to = op->d.jsonexpr_behavior.jump_onempty_expr; + } + else if (!post_eval->coercion_done) + { + /* + * If no error or the JSON item is not empty, directly go to the + * coercion step to coerce the item as is. + */ + return op->d.jsonexpr_behavior.jump_coercion; + } + + Assert(behavior); + + /* + * Set up for coercion step that will run to coerce a non-default behavior + * value. It should use result_coercion, if any. Errors that may occur + * should be thrown for JSON ops other than JSON_VALUE_OP. + */ + if (behavior->btype != JSON_BEHAVIOR_DEFAULT) + { + post_eval->item_jcstate = NULL; + jsestate->throw_error = (jsestate->jsexpr->op != JSON_VALUE_OP); + } + + Assert(jump_to >= 0); + return jump_to; +} + +/* + * Evaluate or return the step address to evaluate a coercion of a JSON item + * to the target type. The former if the coercion must be done right away by + * calling its input function or by calling json_populate_type(). + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull) +{ + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonExpr *jexpr = jsestate->jsexpr; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonCoercionState *item_jcstate = post_eval->item_jcstate; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + + if (item_jcstate == NULL && + jsestate->jsexpr->op != JSON_EXISTS_OP) + { + ErrorSaveContext escontext = {T_ErrorSaveContext}; + Node *escontext_p; + JsonCoercion *coercion; + Jsonb *jb; + + escontext_p = !jsestate->throw_error ? (Node *) &escontext : NULL; + coercion = result_jcstate ? result_jcstate->coercion : NULL; + jb = resnull ? NULL : DatumGetJsonbP(res); + + if ((coercion && coercion->via_io) || + (jexpr->omit_quotes && !resnull && + JB_ROOT_IS_SCALAR(jb))) + { + /* strip quotes and call typinput function */ + char *str = resnull ? NULL : JsonbUnquote(jb); + bool type_is_domain; + + type_is_domain = (getBaseType(jexpr->returning->typid) != + jexpr->returning->typid); + + /* + * Catch errors only if the type is not a domain, because errors + * caused by a domain's constraint failure must be thrown right + * away. + */ + if (!InputFunctionCallSafe(jsestate->input.finfo, str, + jsestate->input.typioparam, + jexpr->returning->typmod, + !type_is_domain ? escontext_p : NULL, + op->resvalue)) + { + post_eval->error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + else if (coercion && coercion->via_populate) + { + *op->resvalue = json_populate_type(res, JSONBOID, + jexpr->returning->typid, + jexpr->returning->typmod, + &post_eval->cache, + econtext->ecxt_per_query_memory, + op->resnull, + escontext_p); + if (SOFT_ERROR_OCCURRED(escontext_p)) + { + post_eval->error = true; + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + } + + if (!jsestate->throw_error) + { + post_eval->escontext.type = T_ErrorSaveContext; + state->escontext = (Node *) &post_eval->escontext; + } + + if (item_jcstate && item_jcstate->jump_eval_expr >= 0) + return item_jcstate->jump_eval_expr; + else if (result_jcstate && result_jcstate->jump_eval_expr >= 0) + return result_jcstate->jump_eval_expr; + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; +} + +/* + * Checks if the coercion evaluation led to an error. If an error did occur, + * this returns the address of the step that handles the error, otherwise + * the step after the coercion step, which finishes the JsonExpr processing. + */ +int +ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op) +{ + JsonExprPostEvalState *post_eval = + &op->d.jsonexpr_coercion_finish.jsestate->post_eval; + + if (SOFT_ERROR_OCCURRED(state->escontext)) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + post_eval->coercion_error = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_error; + } + + /* Reset. */ + state->escontext = NULL; + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_done; +} + +/* + * Prepare SQL/JSON item coercion to the output type. Returned a datum of the + * corresponding SQL type and a pointer to the coercion state. + */ +static Datum +ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate) +{ + JsonCoercionState *item_jcstate; + Datum res; + JsonbValue buf; + + if (item->type == jbvBinary && + JsonContainerIsScalar(item->val.binary.data)) + { + bool res PG_USED_FOR_ASSERTS_ONLY; + + res = JsonbExtractScalar(item->val.binary.data, &buf); + item = &buf; + Assert(res); + } + + /* get coercion state reference and datum of the corresponding SQL type */ + switch (item->type) + { + case jbvNull: + item_jcstate = item_jcstates->null; + res = (Datum) 0; + break; + + case jbvString: + item_jcstate = item_jcstates->string; + res = PointerGetDatum(cstring_to_text_with_len(item->val.string.val, + item->val.string.len)); + break; + + case jbvNumeric: + item_jcstate = item_jcstates->numeric; + res = NumericGetDatum(item->val.numeric); + break; + + case jbvBool: + item_jcstate = item_jcstates->boolean; + res = BoolGetDatum(item->val.boolean); + break; + + case jbvDatetime: + res = item->val.datetime.value; + switch (item->val.datetime.typid) + { + case DATEOID: + item_jcstate = item_jcstates->date; + break; + case TIMEOID: + item_jcstate = item_jcstates->time; + break; + case TIMETZOID: + item_jcstate = item_jcstates->timetz; + break; + case TIMESTAMPOID: + item_jcstate = item_jcstates->timestamp; + break; + case TIMESTAMPTZOID: + item_jcstate = item_jcstates->timestamptz; + break; + default: + elog(ERROR, "unexpected jsonb datetime type oid %u", + item->val.datetime.typid); + return (Datum) 0; + } + break; + + case jbvArray: + case jbvObject: + case jbvBinary: + item_jcstate = item_jcstates->composite; + res = JsonbPGetDatum(JsonbValueToJsonb(item)); + break; + + default: + elog(ERROR, "unexpected jsonb value type %d", item->type); + return (Datum) 0; + } + + *p_item_jcstate = item_jcstate; + + return res; +} + /* * ExecEvalGroupingFunc diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 00d7b8110b..da3870cba6 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -1860,6 +1860,256 @@ llvm_compile_expr(ExprState *state) LLVMBuildBr(b, opblocks[opno + 1]); break; + case EEOP_JSONEXPR_PATH: + build_EvalXFunc(b, mod, "ExecEvalJsonExpr", + v_state, op, v_econtext); + LLVMBuildBr(b, opblocks[opno + 1]); + break; + + case EEOP_JSONEXPR_SKIP: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprSkip() to decide if JSON path + * evaluation can be skipped. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprSkip"), + params, lengthof(params), ""); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_skip.jump_coercion, which signifies + * skipping of JSON path evaluation, else to the next step + * which must point to the steps to evaluate PASSING args, + * if any, or to the JSON path evaluation. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_skip.jump_coercion), + ""), + opblocks[op->d.jsonexpr_skip.jump_coercion], + opblocks[opno + 1]); + break; + } + + case EEOP_JSONEXPR_BEHAVIOR: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + LLVMBasicBlockRef b_jump_onerror_default; + + /* + * Call ExecEvalJsonExprBehavior() to decide if ON EMPTY + * or ON ERROR behavior must be invoked depending on what + * JSON path evaluation returned. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprBehavior"), + params, lengthof(params), ""); + + b_jump_onerror_default = + l_bb_before_v(opblocks[opno + 1], + "op.%d.jsonexpr_behavior_jump_onerror_default", opno); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_behavior.jump_coercion, else to the + * next block, one that checks whether to evaluate the ON + * ERROR default expression. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_coercion), + ""), + opblocks[op->d.jsonexpr_behavior.jump_coercion], + b_jump_onerror_default); + + /* + * Block that checks whether to evaluate the ON ERROR + * default expression. + * + * Jump to evaluate the ON ERROR default expression if the + * returned address is the same as + * jsonexpr_behavior.jump_onerror_default, else jump to + * evaluate the ON EMPTY default expression. + */ + LLVMPositionBuilderAtEnd(b, b_jump_onerror_default); + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_onerror_expr), + ""), + opblocks[op->d.jsonexpr_behavior.jump_onerror_expr], + opblocks[op->d.jsonexpr_behavior.jump_onempty_expr]); + break; + } + case EEOP_JSONEXPR_COERCION: + { + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonItemCoercionsState *item_jcstates = jsestate->item_jcstates; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + LLVMValueRef v_ret; + LLVMValueRef params[5]; + + /* + * Call ExecEvalJsonExprCoercion() to evaluate appropriate + * coercion. This will return the step address to jump + * to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + params[2] = v_econtext; + params[3] = v_resvaluep; + params[4] = v_resnullp; + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercion"), + params, lengthof(params), ""); + + /* + * Jump to handle a coercion error if the returned address + * is the same as jsonexpr_coercion.jump_coercion_error, + * else to the step after coercion (coercion done!). + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + + if (item_jcstates) + { + JsonCoercionState **item_jcstate; + int n_coercions = (int) + (item_jcstates->composite - item_jcstates->null) + 1; + int i; + LLVMBasicBlockRef *b_coercions; + + + /* + * Will create a block for each coercion below to + * check whether to evaluate the coercion's expression + * if there's one or to skip to the end if not. + */ + b_coercions = palloc((n_coercions + 1) * sizeof(LLVMBasicBlockRef)); + for (i = 0; i < n_coercions + 1; i++) + b_coercions[i] = + l_bb_before_v(opblocks[opno + 1], + "op.%d.json_item_coercion.%d", + opno, i); + + /* Jump to check first coercion */ + LLVMBuildBr(b, b_coercions[0]); + + /* + * Add conditional branches for individual coercion's + * expressions + */ + for (item_jcstate = &item_jcstates->null, i = 0; + item_jcstate <= &item_jcstates->composite; + item_jcstate++, i++) + { + /* Block for this coercion */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + + /* + * Jump to evaluate the coercion's expression if + * the address returned is the same as this + * coercion's jump_eval_expr (that is, if it is + * valid), else check the next coercion's. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const((*item_jcstate)->jump_eval_expr), + ""), + (*item_jcstate)->jump_eval_expr >= 0 ? + opblocks[(*item_jcstate)->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + b_coercions[i + 1]); + } + + /* + * A placeholder block that the last coercion's block + * might jump to, which unconditionally jumps to end + * of coercions. + */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + LLVMBuildBr(b, opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + + /* + * Jump to evaluate the result_coercion's expression if + * none of the above coercions matched, that is, if + * there's one. + */ + if (result_jcstate) + { + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(result_jcstate->jump_eval_expr), + ""), + result_jcstate->jump_eval_expr >= 0 ? + opblocks[result_jcstate->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + break; + } + + case EEOP_JSONEXPR_COERCION_FINISH: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprCoercionFinish() to check whether + * an coercion error occurred, in which case we must jump + * to whatever step handles the error. This returns the + * step address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercionFinish"), + params, lengthof(params), ""); + + /* + * Jump to the step that handles coercion error if the + * returned address is the same as + * jsonexpr_coercion_finish.jump_coercion_error, else to + * jsonexpr_coercion_finish.jump_coercion_done. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion_finish.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_done]); + break; + } + case EEOP_AGGREF: { LLVMValueRef v_aggno; diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c index 41ac4c6f45..cf3ced3427 100644 --- a/src/backend/jit/llvm/llvmjit_types.c +++ b/src/backend/jit/llvm/llvmjit_types.c @@ -135,6 +135,11 @@ void *referenced_functions[] = ExecEvalXmlExpr, ExecEvalJsonConstructor, ExecEvalJsonIsPredicate, + ExecEvalJsonExprSkip, + ExecEvalJsonExprBehavior, + ExecEvalJsonExprCoercion, + ExecEvalJsonExprCoercionFinish, + ExecEvalJsonExpr, MakeExpandedObjectReadOnlyInternal, slot_getmissingattrs, slot_getsomeattrs_int, diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 39e1884cf4..5dc3aa2e9f 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -859,6 +859,21 @@ makeJsonValueExpr(Expr *expr, JsonFormat *format) return jve; } +/* + * makeJsonBehavior - + * creates a JsonBehavior node + */ +JsonBehavior * +makeJsonBehavior(JsonBehaviorType type, Node *default_expr) +{ + JsonBehavior *behavior = makeNode(JsonBehavior); + + behavior->btype = type; + behavior->default_expr = default_expr; + + return behavior; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 5e9c46005d..3e3fa06b02 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -236,6 +236,12 @@ exprType(const Node *expr) case T_JsonIsPredicate: type = BOOLOID; break; + case T_JsonExpr: + type = ((const JsonExpr *) expr)->returning->typid; + break; + case T_JsonCoercion: + type = exprType(((const JsonCoercion *) expr)->expr); + break; case T_NullTest: type = BOOLOID; break; @@ -495,6 +501,10 @@ exprTypmod(const Node *expr) return exprTypmod((Node *) ((const JsonValueExpr *) expr)->formatted_expr); case T_JsonConstructorExpr: return ((const JsonConstructorExpr *) expr)->returning->typmod; + case T_JsonExpr: + return ((JsonExpr *) expr)->returning->typmod; + case T_JsonCoercion: + return exprTypmod(((const JsonCoercion *) expr)->expr); case T_CoerceToDomain: return ((const CoerceToDomain *) expr)->resulttypmod; case T_CoerceToDomainValue: @@ -971,6 +981,22 @@ exprCollation(const Node *expr) /* IS JSON's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + coll = InvalidOid; + else if (coercion->expr) + coll = exprCollation(coercion->expr); + else if (coercion->via_io || coercion->via_populate) + coll = coercion->collation; + else + coll = InvalidOid; + } + break; + case T_NullTest: /* NullTest's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ @@ -1207,6 +1233,21 @@ exprSetCollation(Node *expr, Oid collation) case T_JsonIsPredicate: Assert(!OidIsValid(collation)); /* result is always boolean */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + Assert(!OidIsValid(collation)); + else if (coercion->expr) + exprSetCollation(coercion->expr, collation); + else if (coercion->via_io || coercion->via_populate) + coercion->collation = collation; + else + Assert(!OidIsValid(collation)); + } + break; case T_NullTest: /* NullTest's result is boolean ... */ Assert(!OidIsValid(collation)); /* ... so never set a collation */ @@ -1510,6 +1551,15 @@ exprLocation(const Node *expr) case T_JsonIsPredicate: loc = ((const JsonIsPredicate *) expr)->location; break; + case T_JsonExpr: + { + const JsonExpr *jsexpr = (const JsonExpr *) expr; + + /* consider both function name and leftmost arg */ + loc = leftmostLoc(jsexpr->location, + exprLocation(jsexpr->formatted_expr)); + } + break; case T_NullTest: { const NullTest *nexpr = (const NullTest *) expr; @@ -2262,6 +2312,54 @@ expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + if (WALK(jexpr->formatted_expr)) + return true; + if (WALK(jexpr->result_coercion)) + return true; + if (WALK(jexpr->passing_values)) + return true; + /* we assume walker doesn't care about passing_names */ + if (jexpr->on_empty && + WALK(jexpr->on_empty->default_expr)) + return true; + if (WALK(jexpr->on_error->default_expr)) + return true; + if (WALK(jexpr->coercions)) + return true; + } + break; + case T_JsonCoercion: + return WALK(((JsonCoercion *) node)->expr); + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + + if (WALK(coercions->null)) + return true; + if (WALK(coercions->string)) + return true; + if (WALK(coercions->numeric)) + return true; + if (WALK(coercions->boolean)) + return true; + if (WALK(coercions->date)) + return true; + if (WALK(coercions->time)) + return true; + if (WALK(coercions->timetz)) + return true; + if (WALK(coercions->timestamp)) + return true; + if (WALK(coercions->timestamptz)) + return true; + if (WALK(coercions->composite)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: @@ -3261,6 +3359,54 @@ expression_tree_mutator_impl(Node *node, return (Node *) newnode; } + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + JsonExpr *newnode; + + FLATCOPY(newnode, jexpr, JsonExpr); + MUTATE(newnode->path_spec, jexpr->path_spec, Node *); + MUTATE(newnode->formatted_expr, jexpr->formatted_expr, Node *); + MUTATE(newnode->result_coercion, jexpr->result_coercion, JsonCoercion *); + MUTATE(newnode->passing_values, jexpr->passing_values, List *); + /* assume mutator does not care about passing_names */ + if (newnode->on_empty) + MUTATE(newnode->on_empty->default_expr, + jexpr->on_empty->default_expr, Node *); + MUTATE(newnode->on_error->default_expr, + jexpr->on_error->default_expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonCoercion: + { + JsonCoercion *coercion = (JsonCoercion *) node; + JsonCoercion *newnode; + + FLATCOPY(newnode, coercion, JsonCoercion); + MUTATE(newnode->expr, coercion->expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + JsonItemCoercions *newnode; + + FLATCOPY(newnode, coercions, JsonItemCoercions); + MUTATE(newnode->null, coercions->null, JsonCoercion *); + MUTATE(newnode->string, coercions->string, JsonCoercion *); + MUTATE(newnode->numeric, coercions->numeric, JsonCoercion *); + MUTATE(newnode->boolean, coercions->boolean, JsonCoercion *); + MUTATE(newnode->date, coercions->date, JsonCoercion *); + MUTATE(newnode->time, coercions->time, JsonCoercion *); + MUTATE(newnode->timetz, coercions->timetz, JsonCoercion *); + MUTATE(newnode->timestamp, coercions->timestamp, JsonCoercion *); + MUTATE(newnode->timestamptz, coercions->timestamptz, JsonCoercion *); + MUTATE(newnode->composite, coercions->composite, JsonCoercion *); + return (Node *) newnode; + } + break; case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -3947,6 +4093,43 @@ raw_expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonArgument: + return WALK(((JsonArgument *) node)->val); + case T_JsonCommon: + { + JsonCommon *jc = (JsonCommon *) node; + + if (WALK(jc->expr)) + return true; + if (WALK(jc->pathspec)) + return true; + if (WALK(jc->passing)) + return true; + } + break; + case T_JsonBehavior: + { + JsonBehavior *jb = (JsonBehavior *) node; + + if (jb->btype == JSON_BEHAVIOR_DEFAULT && + WALK(jb->default_expr)) + return true; + } + break; + case T_JsonFuncExpr: + { + JsonFuncExpr *jfe = (JsonFuncExpr *) node; + + if (WALK(jfe->common)) + return true; + if (jfe->output && WALK(jfe->output)) + return true; + if (WALK(jfe->on_empty)) + return true; + if (WALK(jfe->on_error)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index ef475d95a1..f58c275b4b 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -4609,7 +4609,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context) IsA(node, SQLValueFunction) || IsA(node, XmlExpr) || IsA(node, CoerceToDomain) || - IsA(node, NextValueExpr)) + IsA(node, NextValueExpr) || + IsA(node, JsonExpr)) { /* Treat all these as having cost 1 */ context->total.per_tuple += cpu_operator_cost; diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 7f453b04f8..23a138571d 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -53,6 +53,7 @@ #include "utils/fmgroids.h" #include "utils/json.h" #include "utils/jsonb.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/syscache.h" @@ -412,6 +413,24 @@ contain_mutable_functions_walker(Node *node, void *context) /* Check all subnodes */ } + if (IsA(node, JsonExpr)) + { + JsonExpr *jexpr = castNode(JsonExpr, node); + Const *cnst; + + if (!IsA(jexpr->path_spec, Const)) + return true; + + cnst = castNode(Const, jexpr->path_spec); + + Assert(cnst->consttype == JSONPATHOID); + if (cnst->constisnull) + return false; + + return jspIsMutable(DatumGetJsonPathP(cnst->constvalue), + jexpr->passing_names, jexpr->passing_values); + } + if (IsA(node, SQLValueFunction)) { /* all variants of SQLValueFunction are stable */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 8f239de65c..f7f0d5e8bc 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -278,6 +278,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MergeWhenClause *mergewhen; struct KeyActions *keyactions; struct KeyAction *keyaction; + JsonBehavior *jsbehavior; + JsonQuotes js_quotes; } %type <node> stmt toplevel_stmt schema_stmt routine_body_stmt @@ -652,13 +654,26 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_output_clause_opt json_name_and_value json_aggregate_func + json_api_common_syntax + json_argument %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt + json_arguments + json_passing_clause_opt + +%type <str> json_as_path_name_clause_opt %type <ival> json_encoding_clause_opt json_predicate_type_constraint + json_wrapper_behavior + +%type <jsbehavior> json_value_behavior + json_query_behavior + json_exists_behavior + +%type <js_quotes> json_quotes_clause_opt %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt @@ -699,7 +714,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT - COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT + COMMITTED COMPRESSION CONCURRENTLY CONDITIONAL CONFIGURATION CONFLICT CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY COST CREATE CROSS CSV CUBE CURRENT_P CURRENT_CATALOG CURRENT_DATE CURRENT_ROLE CURRENT_SCHEMA @@ -710,8 +725,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P DOUBLE_P DROP - EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT - EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION + EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE + EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION EXTENSION EXTERNAL EXTRACT FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR @@ -726,10 +741,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION - JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG - JSON_SCALAR JSON_SERIALIZE + JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE - KEY KEYS + KEY KEYS KEEP LABEL LANGUAGE LARGE_P LAST_P LATERAL_P LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL @@ -743,7 +758,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC - OBJECT_P OF OFF OFFSET OIDS OLD ON ONLY OPERATOR OPTION OPTIONS OR + OBJECT_P OF OFF OFFSET OIDS OLD OMIT ON ONLY OPERATOR OPTION OPTIONS OR ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER @@ -752,7 +767,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION - QUOTE + QUOTE QUOTES RANGE READ REAL REASSIGN RECHECK RECURSIVE REF_P REFERENCES REFERENCING REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA @@ -763,7 +778,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); SEQUENCE SEQUENCES SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P - START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P + START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRING_P STRIP_P SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN @@ -771,7 +786,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); TREAT TRIGGER TRIM TRUE_P TRUNCATE TRUSTED TYPE_P TYPES_P - UESCAPE UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN + UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING @@ -15668,6 +15683,192 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } + | JSON_QUERY '(' + json_api_common_syntax + json_output_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_output_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_output_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_error = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_output_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->on_error = $10; + n->location = @1; + $$ = (Node *) n; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->location = @1; + $$ = (Node *) p; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + json_exists_behavior ON ERROR_P + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->on_error = $5; + p->location = @1; + $$ = (Node *) p; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->on_error = $8; + n->location = @1; + $$ = (Node *) n; + } ; @@ -16394,6 +16595,48 @@ opt_asymmetric: ASYMMETRIC ; /* SQL/JSON support */ +json_api_common_syntax: + json_value_expr ',' a_expr /* i.e. a json_path */ + json_as_path_name_clause_opt + json_passing_clause_opt + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $4; + n->passing = $5; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_as_path_name_clause_opt: + AS name { $$ = $2; } + | /* EMPTY */ { $$ = NULL; } + ; + +json_passing_clause_opt: + PASSING json_arguments { $$ = $2; } + | /* EMPTY */ { $$ = NIL; } + ; + +json_arguments: + json_argument { $$ = list_make1($1); } + | json_arguments ',' json_argument { $$ = lappend($1, $3); } + ; + +json_argument: + json_value_expr AS ColLabel + { + JsonArgument *n = makeNode(JsonArgument); + + n->val = (JsonValueExpr *) $1; + n->name = $3; + $$ = (Node *) n; + } + ; + json_value_expr: a_expr json_format_clause_opt { @@ -16417,6 +16660,50 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; +json_query_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + | EMPTY_P ARRAY { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + | EMPTY_P OBJECT_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + /* non-standard, for Oracle compatibility only */ + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + ; + +json_exists_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | TRUE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_TRUE, NULL); } + | FALSE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_FALSE, NULL); } + | UNKNOWN { $$ = makeJsonBehavior(JSON_BEHAVIOR_UNKNOWN, NULL); } + ; + +json_value_behavior: + NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + ; + +/* ARRAY is a noise word */ +json_wrapper_behavior: + WITHOUT WRAPPER { $$ = JSW_NONE; } + | WITHOUT ARRAY WRAPPER { $$ = JSW_NONE; } + | WITH WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL ARRAY WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL WRAPPER { $$ = JSW_UNCONDITIONAL; } + | /* empty */ { $$ = JSW_NONE; } + ; + +json_quotes_clause_opt: + KEEP QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_KEEP; } + | KEEP QUOTES { $$ = JS_QUOTES_KEEP; } + | OMIT QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_OMIT; } + | OMIT QUOTES { $$ = JS_QUOTES_OMIT; } + | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } + ; + json_returning_clause_opt: RETURNING Typename { @@ -17033,6 +17320,7 @@ unreserved_keyword: | COMMIT | COMMITTED | COMPRESSION + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17069,10 +17357,12 @@ unreserved_keyword: | DOUBLE_P | DROP | EACH + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17122,6 +17412,7 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION + | KEEP | KEY | KEYS | LABEL @@ -17168,6 +17459,7 @@ unreserved_keyword: | OFF | OIDS | OLD + | OMIT | OPERATOR | OPTION | OPTIONS @@ -17198,6 +17490,7 @@ unreserved_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REASSIGN @@ -17257,6 +17550,7 @@ unreserved_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUPPORT @@ -17279,6 +17573,7 @@ unreserved_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNKNOWN | UNLISTEN @@ -17339,10 +17634,13 @@ col_name_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE | LEAST | NATIONAL | NCHAR @@ -17575,6 +17873,7 @@ bare_label_keyword: | COMMITTED | COMPRESSION | CONCURRENTLY + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17627,11 +17926,13 @@ bare_label_keyword: | DROP | EACH | ELSE + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | END_P | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17701,10 +18002,14 @@ bare_label_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE + | KEEP | KEY | KEYS | LABEL @@ -17765,6 +18070,7 @@ bare_label_keyword: | OFF | OIDS | OLD + | OMIT | ONLY | OPERATOR | OPTION @@ -17802,6 +18108,7 @@ bare_label_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REAL @@ -17870,6 +18177,7 @@ bare_label_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUBSTRING @@ -17904,6 +18212,7 @@ bare_label_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNIQUE | UNKNOWN diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c index 9f6afc351c..692e5d1225 100644 --- a/src/backend/parser/parse_collate.c +++ b/src/backend/parser/parse_collate.c @@ -691,6 +691,13 @@ assign_collations_walker(Node *node, assign_collations_context *context) &loccontext); } break; + case T_JsonExpr: + + /* + * Context item and PASSING arguments are already + * marked with collations in parse_expr.c. + */ + break; default: /* diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 3a44755515..9882f0c217 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -90,6 +90,8 @@ static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); static Node *transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr); +static Node *transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve); +static Node *transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *p); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -353,6 +355,14 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); break; + case T_JsonFuncExpr: + result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); + break; + + case T_JsonValueExpr: + result = transformJsonValueExpr(pstate, (JsonValueExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3239,10 +3249,10 @@ makeCaseTestExpr(Node *expr) * any. */ static Node * -transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, - char *constructName, - JsonFormatType default_format, - Oid targettype) +transformJsonValueExprInternal(ParseState *pstate, JsonValueExpr *ve, + char *constructName, + JsonFormatType default_format, + Oid targettype, bool isarg) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3261,6 +3271,8 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, get_type_category_preferred(exprtype, &typcategory, &typispreferred); + rawexpr = expr; + if (ve->format->format_type != JS_FORMAT_DEFAULT) { if (ve->format->encoding != JS_ENC_DEFAULT && exprtype != BYTEAOID) @@ -3279,6 +3291,36 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, else format = ve->format->format_type; } + else if (isarg) + { + /* Pass SQL/JSON item types directly without conversion to json[b]. */ + switch (exprtype) + { + case TEXTOID: + case NUMERICOID: + case BOOLOID: + case INT2OID: + case INT4OID: + case INT8OID: + case FLOAT4OID: + case FLOAT8OID: + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + return expr; + + default: + if (typcategory == TYPCATEGORY_STRING) + return coerce_to_specific_type(pstate, expr, TEXTOID, + "JSON_VALUE_EXPR"); + /* else convert argument to json[b] type */ + break; + } + + format = default_format; + } else if (exprtype == JSONOID || exprtype == JSONBOID) format = JS_FORMAT_DEFAULT; /* do not format json[b] types */ else @@ -3290,8 +3332,10 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, Node *coerced; bool cast_is_needed = OidIsValid(targettype); - if (!cast_is_needed && + if (!cast_is_needed && !isarg && exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) + + if (!isarg && exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), errmsg(ve->format->format_type == JS_FORMAT_DEFAULT ? @@ -3355,6 +3399,20 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, return expr; } +/* Wrapper with an interface for use in transformExpr() */ +static Node * +transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve) +{ + /* + * A bit bogus to use "JSON()" here for the parent construct's name but + * there's no good way to know the enclosing JSON expression when called + * through transformExpr(). + */ + return transformJsonValueExprInternal(pstate, ve, "JSON()", + JS_FORMAT_DEFAULT, + InvalidOid, false); +} + /* * Checks specified output format for its applicability to the target type. */ @@ -3614,10 +3672,12 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) { JsonKeyValue *kv = castNode(JsonKeyValue, lfirst(lc)); Node *key = transformExprRecurse(pstate, (Node *) kv->key); - Node *val = transformJsonValueExpr(pstate, kv->value, - "JSON_OBJECT()", - JS_FORMAT_DEFAULT, - InvalidOid); + Node *val = transformJsonValueExprInternal(pstate, + kv->value, + "JSON_OBJECT()", + JS_FORMAT_DEFAULT, + InvalidOid, + false); args = lappend(args, key); args = lappend(args, val); @@ -3796,8 +3856,10 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) Oid aggtype; key = transformExprRecurse(pstate, (Node *) agg->arg->key); - val = transformJsonValueExpr(pstate, agg->arg->value, "JSON_OBJECTAGG()", - JS_FORMAT_DEFAULT, InvalidOid); + val = transformJsonValueExprInternal(pstate, agg->arg->value, + "JSON_OBJECTAGG()", + JS_FORMAT_DEFAULT, + InvalidOid, false); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3853,8 +3915,8 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) Oid aggfnoid; Oid aggtype; - arg = transformJsonValueExpr(pstate, agg->arg, "JSON_ARRAYAGG()", - JS_FORMAT_DEFAULT, InvalidOid); + arg = transformJsonValueExprInternal(pstate, agg->arg, "JSON_ARRAYAGG()", + JS_FORMAT_DEFAULT, InvalidOid, false); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3900,10 +3962,10 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) foreach(lc, ctor->exprs) { JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); - Node *val = transformJsonValueExpr(pstate, jsval, - "JSON_ARRAY()", - JS_FORMAT_DEFAULT, - InvalidOid); + Node *val = transformJsonValueExprInternal(pstate, jsval, + "JSON_ARRAY()", + JS_FORMAT_DEFAULT, + InvalidOid, false); args = lappend(args, val); } @@ -4058,8 +4120,9 @@ transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) * Coerce argument to target type using CAST for compatibility with PG * function-like CASTs. */ - arg = transformJsonValueExpr(pstate, jsexpr->expr, "JSON()", - JS_FORMAT_JSON, returning->typid); + arg = transformJsonValueExprInternal(pstate, jsexpr->expr, "JSON()", + JS_FORMAT_JSON, returning->typid, + false); } return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, @@ -4090,10 +4153,10 @@ transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *jsexpr) static Node * transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) { - Node *arg = transformJsonValueExpr(pstate, expr->expr, - "JSON_SERIALIZE()", - JS_FORMAT_JSON, - InvalidOid); + Node *arg = transformJsonValueExprInternal(pstate, expr->expr, + "JSON_SERIALIZE()", + JS_FORMAT_JSON, + InvalidOid, false); JsonReturning *returning; if (expr->output) @@ -4128,3 +4191,438 @@ transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), NULL, returning, false, false, expr->location); } + +/* + * Transform a JSON PASSING clause. + */ +static void +transformJsonPassingArgs(ParseState *pstate, JsonFormatType format, List *args, + List **passing_values, List **passing_names) +{ + ListCell *lc; + + *passing_values = NIL; + *passing_names = NIL; + + foreach(lc, args) + { + JsonArgument *arg = castNode(JsonArgument, lfirst(lc)); + Node *expr = transformJsonValueExprInternal(pstate, arg->val, + "JSON PASSING()", + format, InvalidOid, + true); + + assign_expr_collations(pstate, expr); + + *passing_values = lappend(*passing_values, expr); + *passing_names = lappend(*passing_names, makeString(arg->name)); + } +} + +/* + * Transform a JSON BEHAVIOR clause. + */ +static JsonBehavior * +transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior, + JsonBehaviorType default_behavior) +{ + JsonBehaviorType behavior_type = default_behavior; + Node *default_expr = NULL; + + if (behavior) + { + behavior_type = behavior->btype; + if (behavior_type == JSON_BEHAVIOR_DEFAULT) + default_expr = transformExprRecurse(pstate, behavior->default_expr); + } + return makeJsonBehavior(behavior_type, default_expr); +} + +/* + * Common code for JSON_VALUE, JSON_QUERY, JSON_EXISTS transformation + * into a JsonExpr node. + */ +static JsonExpr * +transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = makeNode(JsonExpr); + Node *pathspec; + JsonFormatType format; + char *constructName; + + if (func->common->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("JSON_TABLE path name is not allowed here"), + parser_errposition(pstate, func->location))); + + jsexpr->location = func->location; + jsexpr->op = func->op; + switch (jsexpr->op) + { + case JSON_VALUE_OP: + constructName = "JSON_VALUE()"; + break; + case JSON_QUERY_OP: + constructName = "JSON_QUERY()"; + break; + case JSON_EXISTS_OP: + constructName = "JSON_EXISTS()"; + break; + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); + break; + } + jsexpr->formatted_expr = transformJsonValueExprInternal(pstate, + func->common->expr, + constructName, + JS_FORMAT_DEFAULT, + InvalidOid, false); + + assign_expr_collations(pstate, jsexpr->formatted_expr); + + /* format is determined by context item type */ + format = exprType(jsexpr->formatted_expr) == JSONBOID ? JS_FORMAT_JSONB : JS_FORMAT_JSON; + + jsexpr->result_coercion = NULL; + jsexpr->omit_quotes = false; + + jsexpr->format = func->common->expr->format; + + pathspec = transformExprRecurse(pstate, func->common->pathspec); + + jsexpr->path_spec = + coerce_to_target_type(pstate, pathspec, exprType(pathspec), + JSONPATHOID, -1, + COERCION_EXPLICIT, COERCE_IMPLICIT_CAST, + exprLocation(pathspec)); + if (!jsexpr->path_spec) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("JSON path expression must be type %s, not type %s", + "jsonpath", format_type_be(exprType(pathspec))), + parser_errposition(pstate, exprLocation(pathspec)))); + + /* transform and coerce to json[b] passing arguments */ + transformJsonPassingArgs(pstate, format, func->common->passing, + &jsexpr->passing_values, &jsexpr->passing_names); + + if (func->op != JSON_EXISTS_OP) + jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, + JSON_BEHAVIOR_NULL); + + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + func->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : + JSON_BEHAVIOR_NULL); + + return jsexpr; +} + +/* + * Assign default JSON returning type from the specified format or from + * the context item type. + */ +static void +assignDefaultJsonReturningType(Node *context_item, JsonFormat *context_format, + JsonReturning *ret) +{ + bool is_jsonb; + + ret->format = copyObject(context_format); + + if (ret->format->format_type == JS_FORMAT_DEFAULT) + is_jsonb = exprType(context_item) == JSONBOID; + else + is_jsonb = ret->format->format_type == JS_FORMAT_JSONB; + + ret->typid = is_jsonb ? JSONBOID : JSONOID; + ret->typmod = -1; +} + +/* + * Try to coerce expression to the output type or + * use json_populate_type() for composite, array and domain types or + * use coercion via I/O. + */ +static JsonCoercion * +coerceJsonExpr(ParseState *pstate, Node *expr, const JsonReturning *returning) +{ + char typtype; + JsonCoercion *coercion = makeNode(JsonCoercion); + + coercion->expr = coerceJsonFuncExpr(pstate, expr, returning, false); + + if (coercion->expr) + { + if (coercion->expr == expr) + coercion->expr = NULL; + + return coercion; + } + + typtype = get_typtype(returning->typid); + + if (returning->typid == RECORDOID || + typtype == TYPTYPE_COMPOSITE || + typtype == TYPTYPE_DOMAIN || + type_is_array(returning->typid)) + coercion->via_populate = true; + else + coercion->via_io = true; + + return coercion; +} + +/* + * Transform a JSON output clause of JSON_VALUE and JSON_QUERY. + */ +static void +transformJsonFuncExprOutput(ParseState *pstate, JsonFuncExpr *func, + JsonExpr *jsexpr) +{ + Node *expr = jsexpr->formatted_expr; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + /* JSON_VALUE returns text by default */ + if (func->op == JSON_VALUE_OP && !OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = TEXTOID; + jsexpr->returning->typmod = -1; + } + + if (OidIsValid(jsexpr->returning->typid)) + { + JsonReturning ret; + + if (func->op == JSON_VALUE_OP && + jsexpr->returning->typid != JSONOID && + jsexpr->returning->typid != JSONBOID) + { + /* Forced coercion via I/O for JSON_VALUE for non-JSON types */ + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = NULL; + jsexpr->result_coercion->via_io = true; + return; + } + + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, &ret); + + if (ret.typid != jsexpr->returning->typid || + ret.typmod != jsexpr->returning->typmod) + { + Node *placeholder = makeCaseTestExpr(expr); + + Assert(((CaseTestExpr *) placeholder)->typeId == ret.typid); + Assert(((CaseTestExpr *) placeholder)->typeMod == ret.typmod); + + jsexpr->result_coercion = coerceJsonExpr(pstate, placeholder, + jsexpr->returning); + } + } + else + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, + jsexpr->returning); +} + +/* + * Coerce an expression in JSON DEFAULT behavior to the target output type. + */ +static Node * +coerceDefaultJsonExpr(ParseState *pstate, JsonExpr *jsexpr, Node *defexpr) +{ + int location; + Oid exprtype; + + if (!defexpr) + return NULL; + + exprtype = exprType(defexpr); + location = exprLocation(defexpr); + + if (location < 0) + location = jsexpr->location; + + defexpr = coerce_to_target_type(pstate, + defexpr, + exprtype, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!defexpr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast DEFAULT expression type %s to %s", + format_type_be(exprtype), + format_type_be(jsexpr->returning->typid)), + parser_errposition(pstate, location))); + + return defexpr; +} + +/* + * Initialize SQL/JSON item coercion from the SQL type "typid" to the target + * "returning" type. + */ +static JsonCoercion * +initJsonItemCoercion(ParseState *pstate, Oid typid, + const JsonReturning *returning) +{ + Node *expr; + + if (typid == UNKNOWNOID) + { + expr = (Node *) makeNullConst(UNKNOWNOID, -1, InvalidOid); + } + else + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + + placeholder->typeId = typid; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + expr = (Node *) placeholder; + } + + return coerceJsonExpr(pstate, expr, returning); +} + +static void +initJsonItemCoercions(ParseState *pstate, JsonItemCoercions *coercions, + const JsonReturning *returning, Oid contextItemTypeId) +{ + struct + { + JsonCoercion **coercion; + Oid typid; + } *p, + coercionTypids[] = + { + {&coercions->null, UNKNOWNOID}, + {&coercions->string, TEXTOID}, + {&coercions->numeric, NUMERICOID}, + {&coercions->boolean, BOOLOID}, + {&coercions->date, DATEOID}, + {&coercions->time, TIMEOID}, + {&coercions->timetz, TIMETZOID}, + {&coercions->timestamp, TIMESTAMPOID}, + {&coercions->timestamptz, TIMESTAMPTZOID}, + {&coercions->composite, contextItemTypeId}, + {NULL, InvalidOid} + }; + + for (p = coercionTypids; p->coercion; p++) + *p->coercion = initJsonItemCoercion(pstate, p->typid, returning); +} + +/* + * Transform JSON_VALUE, JSON_QUERY, JSON_EXISTS functions into a JsonExpr node. + */ +static Node * +transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = transformJsonExprCommon(pstate, func); + const char *func_name = NULL; + Node *contextItemExpr = jsexpr->formatted_expr; + + switch (func->op) + { + case JSON_VALUE_OP: + func_name = "JSON_VALUE"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->coercions = makeNode(JsonItemCoercions); + initJsonItemCoercions(pstate, jsexpr->coercions, jsexpr->returning, + exprType(contextItemExpr)); + + break; + + case JSON_QUERY_OP: + func_name = "JSON_QUERY"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->wrapper = func->wrapper; + jsexpr->omit_quotes = func->omit_quotes; + + break; + + case JSON_EXISTS_OP: + func_name = "JSON_EXISTS"; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + if (!OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = BOOLOID; + jsexpr->returning->typmod = -1; + } + else if (jsexpr->returning->typid != BOOLOID) + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + int location = exprLocation((Node *) jsexpr); + + placeholder->typeId = BOOLOID; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = + coerce_to_target_type(pstate, (Node *) placeholder, BOOLOID, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!jsexpr->result_coercion->expr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(BOOLOID), + format_type_be(jsexpr->returning->typid)), + parser_coercion_errposition(pstate, location, (Node *) jsexpr))); + + if (jsexpr->result_coercion->expr == (Node *) placeholder) + jsexpr->result_coercion->expr = NULL; + } + break; + } + + if (exprType(contextItemExpr) != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("%s() is not yet implemented for the json type", func_name), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + return (Node *) jsexpr; +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 520d4f2a23..4f94fc69d6 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1979,6 +1979,21 @@ FigureColnameInternal(Node *node, char **name) /* make JSON_ARRAYAGG act like a regular function */ *name = "json_arrayagg"; return 2; + case T_JsonFuncExpr: + /* make SQL/JSON functions act like a regular function */ + switch (((JsonFuncExpr *) node)->op) + { + case JSON_QUERY_OP: + *name = "json_query"; + return 2; + case JSON_VALUE_OP: + *name = "json_value"; + return 2; + case JSON_EXISTS_OP: + *name = "json_exists"; + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index e6246dc44b..f6015debbb 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -4444,6 +4444,50 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, } } +/* + * Parses the datetime format string in 'fmt_str' and returns true if it + * contains a timezone specifier, false if not. + */ +bool +datetime_format_has_tz(const char *fmt_str) +{ + bool incache; + int fmt_len = strlen(fmt_str); + int result; + FormatNode *format; + + if (fmt_len > DCH_CACHE_SIZE) + { + /* + * Allocate new memory if format picture is bigger than static cache + * and do not use cache (call parser always) + */ + incache = false; + + format = (FormatNode *) palloc((fmt_len + 1) * sizeof(FormatNode)); + + parse_format(format, fmt_str, DCH_keywords, + DCH_suff, DCH_index, DCH_FLAG, NULL); + } + else + { + /* + * Use cache buffers + */ + DCHCacheEntry *ent = DCH_cache_fetch(fmt_str, false); + + incache = true; + format = ent->format; + } + + result = DCH_datetime_type(format); + + if (!incache) + pfree(format); + + return result & DCH_ZONED; +} + /* * do_to_timestamp: shared code for to_timestamp and to_date * diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index 7a91177a55..100dfa9c3a 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -2265,3 +2265,65 @@ jsonb_float8(PG_FUNCTION_ARGS) PG_RETURN_DATUM(retValue); } + +/* + * Construct an empty array jsonb. + */ +Jsonb * +JsonbMakeEmptyArray(void) +{ + JsonbValue jbv; + + jbv.type = jbvArray; + jbv.val.array.elems = NULL; + jbv.val.array.nElems = 0; + jbv.val.array.rawScalar = false; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Construct an empty object jsonb. + */ +Jsonb * +JsonbMakeEmptyObject(void) +{ + JsonbValue jbv; + + jbv.type = jbvObject; + jbv.val.object.pairs = NULL; + jbv.val.object.nPairs = 0; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Convert jsonb to a C-string stripping quotes from scalar strings. + */ +char * +JsonbUnquote(Jsonb *jb) +{ + if (JB_ROOT_IS_SCALAR(jb)) + { + JsonbValue v; + + (void) JsonbExtractScalar(&jb->root, &v); + + if (v.type == jbvString) + return pnstrdup(v.val.string.val, v.val.string.len); + else if (v.type == jbvBool) + return pstrdup(v.val.boolean ? "true" : "false"); + else if (v.type == jbvNumeric) + return DatumGetCString(DirectFunctionCall1(numeric_out, + PointerGetDatum(v.val.numeric))); + else if (v.type == jbvNull) + return pstrdup("null"); + else + { + elog(ERROR, "unrecognized jsonb value type %d", v.type); + return NULL; + } + } + else + return JsonbToCString(NULL, &jb->root, VARSIZE(jb)); +} diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index 70cb922e6b..cbc6b94fc2 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -264,6 +264,7 @@ typedef struct PopulateArrayContext int *dims; /* dimensions */ int *sizes; /* current dimension counters */ int ndims; /* number of dimensions */ + Node *escontext; /* For soft-error capture */ } PopulateArrayContext; /* state for populate_array_json() */ @@ -441,12 +442,13 @@ static void JsValueToJsObject(JsValue *jsv, JsObject *jso); static Datum populate_composite(CompositeIOData *io, Oid typid, const char *colname, MemoryContext mcxt, HeapTupleHeader defaultval, JsValue *jsv, bool isnull); -static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv); +static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext); static void prepare_column_cache(ColumnIOData *column, Oid typid, int32 typmod, MemoryContext mcxt, bool need_scalar); static Datum populate_record_field(ColumnIOData *col, Oid typid, int32 typmod, const char *colname, MemoryContext mcxt, Datum defaultval, - JsValue *jsv, bool *isnull); + JsValue *jsv, bool *isnull, Node *escontext); static RecordIOData *allocate_record_info(MemoryContext mcxt, int ncolumns); static bool JsObjectGetField(JsObject *obj, char *field, JsValue *jsv); static void populate_recordset_record(PopulateRecordsetState *state, JsObject *obj); @@ -458,7 +460,8 @@ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims); static void populate_array_check_dimension(PopulateArrayContext *ctx, int ndim); static void populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv); static Datum populate_array(ArrayIOData *aio, const char *colname, - MemoryContext mcxt, JsValue *jsv); + MemoryContext mcxt, JsValue *jsv, Node *escontext, + bool *isnull); static Datum populate_domain(DomainIOData *io, Oid typid, const char *colname, MemoryContext mcxt, JsValue *jsv, bool isnull); @@ -2483,12 +2486,12 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) if (ndim <= 0) { if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the value of key \"%s\".", ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"))); } @@ -2505,13 +2508,13 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) appendStringInfo(&indices, "[%d]", ctx->sizes[i]); if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s of key \"%s\".", indices.data, ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s.", @@ -2519,7 +2522,11 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) } } -/* set the number of dimensions of the populated array when it becomes known */ +/* + * Set the number of dimensions of the populated array when it becomes known. + * + * Returns without doing anything if the input (ndims) is erratic. + */ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) { @@ -2530,6 +2537,10 @@ populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) if (ndims <= 0) populate_array_report_expected_array(ctx, ndims); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + ctx->ndims = ndims; ctx->dims = palloc(sizeof(int) * ndims); ctx->sizes = palloc0(sizeof(int) * ndims); @@ -2547,12 +2558,16 @@ populate_array_check_dimension(PopulateArrayContext *ctx, int ndim) if (ctx->dims[ndim] == -1) ctx->dims[ndim] = dim; /* assign dimension if not yet known */ else if (ctx->dims[ndim] != dim) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("malformed JSON array"), errdetail("Multidimensional arrays must have " "sub-arrays with matching dimensions."))); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + /* reset the current array dimension size counter */ ctx->sizes[ndim] = 0; @@ -2572,7 +2587,10 @@ populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv) ctx->aio->element_type, ctx->aio->element_typmod, NULL, ctx->mcxt, PointerGetDatum(NULL), - jsv, &element_isnull); + jsv, &element_isnull, ctx->escontext); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; accumArrayResult(ctx->astate, element, element_isnull, ctx->aio->element_type, ctx->acxt); @@ -2593,6 +2611,10 @@ populate_array_object_start(void *_state) else if (ndim < state->ctx->ndims) populate_array_report_expected_array(state->ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2610,6 +2632,10 @@ populate_array_array_end(void *_state) if (ndim < ctx->ndims) populate_array_check_dimension(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2685,6 +2711,10 @@ populate_array_scalar(void *_state, char *token, JsonTokenType tokentype) else if (ndim < ctx->ndims) populate_array_report_expected_array(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + if (ndim == ctx->ndims) { /* remember the scalar element token */ @@ -2714,7 +2744,13 @@ populate_array_json(PopulateArrayContext *ctx, char *json, int len) sem.array_element_end = populate_array_element_end; sem.scalar = populate_array_scalar; - pg_parse_json_or_ereport(state.lex, &sem); + if (!pg_parse_json_or_errsave(state.lex, &sem, ctx->escontext)) + { + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + pfree(state.lex); + return; + } /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2739,10 +2775,15 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ check_stack_depth(); - if (jbv->type != jbvBinary || !JsonContainerIsArray(jbc)) + if (jbv->type != jbvBinary || + !JsonContainerIsArray(jbc) || + JsonContainerIsScalar(jbc)) + { populate_array_report_expected_array(ctx, ndim - 1); - - Assert(!JsonContainerIsScalar(jbc)); + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + return; + } it = JsonbIteratorInit(jbc); @@ -2761,7 +2802,12 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ (tok == WJB_ELEM && (val.type != jbvBinary || !JsonContainerIsArray(val.val.binary.data))))) + { populate_array_assign_ndims(ctx, ndim); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + } jsv.is_json = false; jsv.val.jsonb = &val; @@ -2779,6 +2825,9 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ { /* populate child sub-array */ populate_array_dim_jsonb(ctx, &val, ndim + 1); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2796,12 +2845,18 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ Assert(tok == WJB_DONE && !it); } -/* recursively populate an array from json/jsonb */ +/* + * Recursively populate an array from json/jsonb + * + * *isnull is set to true if an error is reported during parsing. + */ static Datum populate_array(ArrayIOData *aio, const char *colname, MemoryContext mcxt, - JsValue *jsv) + JsValue *jsv, + Node *escontext, + bool *isnull) { PopulateArrayContext ctx; Datum result; @@ -2816,6 +2871,7 @@ populate_array(ArrayIOData *aio, ctx.ndims = 0; /* unknown yet */ ctx.dims = NULL; ctx.sizes = NULL; + ctx.escontext = escontext; if (jsv->is_json) populate_array_json(&ctx, jsv->val.json.str, @@ -2824,7 +2880,16 @@ populate_array(ArrayIOData *aio, else { populate_array_dim_jsonb(&ctx, jsv->val.jsonb, 1); - ctx.dims[0] = ctx.sizes[0]; + /* Nothing to do on an error. */ + if (!SOFT_ERROR_OCCURRED(ctx.escontext)) + ctx.dims[0] = ctx.sizes[0]; + } + + /* Nothing to return if an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx.escontext)) + { + *isnull = true; + return (Datum) 0; } Assert(ctx.ndims > 0); @@ -2841,6 +2906,7 @@ populate_array(ArrayIOData *aio, pfree(ctx.sizes); pfree(lbs); + *isnull = false; return result; } @@ -2956,7 +3022,8 @@ populate_composite(CompositeIOData *io, /* populate non-null scalar value from json/jsonb value */ static Datum -populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) +populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext) { Datum res; char *str = NULL; @@ -3027,7 +3094,11 @@ populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) elog(ERROR, "unrecognized jsonb type: %d", (int) jbv->type); } - res = InputFunctionCall(&io->typiofunc, str, io->typioparam, typmod); + if (!InputFunctionCallSafe(&io->typiofunc, str, io->typioparam, typmod, + escontext, &res)) + { + res = (Datum) 0; + } /* free temporary buffer */ if (str != json) @@ -3053,7 +3124,7 @@ populate_domain(DomainIOData *io, res = populate_record_field(io->base_io, io->base_typid, io->base_typmod, colname, mcxt, PointerGetDatum(NULL), - jsv, &isnull); + jsv, &isnull, NULL); Assert(!isnull); } @@ -3158,7 +3229,8 @@ populate_record_field(ColumnIOData *col, MemoryContext mcxt, Datum defaultval, JsValue *jsv, - bool *isnull) + bool *isnull, + Node *escontext) { TypeCat typcat; @@ -3191,10 +3263,12 @@ populate_record_field(ColumnIOData *col, switch (typcat) { case TYPECAT_SCALAR: - return populate_scalar(&col->scalar_io, typid, typmod, jsv); + return populate_scalar(&col->scalar_io, typid, typmod, jsv, + escontext); case TYPECAT_ARRAY: - return populate_array(&col->io.array, colname, mcxt, jsv); + return populate_array(&col->io.array, colname, mcxt, jsv, + escontext, isnull); case TYPECAT_COMPOSITE: case TYPECAT_COMPOSITE_DOMAIN: @@ -3215,6 +3289,53 @@ populate_record_field(ColumnIOData *col, } } +/* recursively populate specified type from a json/jsonb value */ +Datum +json_populate_type(Datum json_val, Oid json_type, Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext) +{ + JsValue jsv = {0}; + JsonbValue jbv; + + jsv.is_json = json_type == JSONOID; + + if (*isnull) + { + if (jsv.is_json) + jsv.val.json.str = NULL; + else + jsv.val.jsonb = NULL; + } + else if (jsv.is_json) + { + text *json = DatumGetTextPP(json_val); + + jsv.val.json.str = VARDATA_ANY(json); + jsv.val.json.len = VARSIZE_ANY_EXHDR(json); + jsv.val.json.type = JSON_TOKEN_INVALID; /* not used in + * populate_composite() */ + } + else + { + Jsonb *jsonb = DatumGetJsonbP(json_val); + + jsv.val.jsonb = &jbv; + + /* fill binary jsonb value pointing to jb */ + jbv.type = jbvBinary; + jbv.val.binary.data = &jsonb->root; + jbv.val.binary.len = VARSIZE(jsonb) - VARHDRSZ; + } + + if (!*cache) + *cache = MemoryContextAllocZero(mcxt, sizeof(ColumnIOData)); + + return populate_record_field(*cache, typid, typmod, NULL, mcxt, + PointerGetDatum(NULL), &jsv, isnull, + escontext); +} + static RecordIOData * allocate_record_info(MemoryContext mcxt, int ncolumns) { @@ -3356,7 +3477,8 @@ populate_record(TupleDesc tupdesc, mcxt, nulls[i] ? (Datum) 0 : values[i], &field, - &nulls[i]); + &nulls[i], + NULL); } res = heap_form_tuple(tupdesc, values, nulls); diff --git a/src/backend/utils/adt/jsonpath.c b/src/backend/utils/adt/jsonpath.c index 7891fde310..5194d0d91f 100644 --- a/src/backend/utils/adt/jsonpath.c +++ b/src/backend/utils/adt/jsonpath.c @@ -68,7 +68,9 @@ #include "libpq/pqformat.h" #include "nodes/miscnodes.h" #include "miscadmin.h" +#include "nodes/nodeFuncs.h" #include "utils/builtins.h" +#include "utils/formatting.h" #include "utils/json.h" #include "utils/jsonpath.h" @@ -1109,3 +1111,256 @@ jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, return true; } + +/* SQL/JSON datatype status: */ +typedef enum JsonPathDatatypeStatus +{ + jpdsNonDateTime, /* null, bool, numeric, string, array, object */ + jpdsUnknownDateTime, /* unknown datetime type */ + jpdsDateTimeZoned, /* timetz, timestamptz */ + jpdsDateTimeNonZoned /* time, timestamp, date */ +} JsonPathDatatypeStatus; + +/* Context for jspIsMutableWalker() */ +typedef struct JsonPathMutableContext +{ + List *varnames; /* list of variable names */ + List *varexprs; /* list of variable expressions */ + JsonPathDatatypeStatus current; /* status of @ item */ + bool lax; /* jsonpath is lax or strict */ + bool mutable; /* resulting mutability status */ +} JsonPathMutableContext; + +/* + * Recursive walker for jspIsMutable() + */ +static JsonPathDatatypeStatus +jspIsMutableWalker(JsonPathItem *jpi, JsonPathMutableContext *cxt) +{ + JsonPathItem next; + JsonPathDatatypeStatus status = jpdsNonDateTime; + + while (!cxt->mutable) + { + JsonPathItem arg; + JsonPathDatatypeStatus leftStatus; + JsonPathDatatypeStatus rightStatus; + + switch (jpi->type) + { + case jpiRoot: + Assert(status == jpdsNonDateTime); + break; + + case jpiCurrent: + Assert(status == jpdsNonDateTime); + status = cxt->current; + break; + + case jpiFilter: + { + JsonPathDatatypeStatus prevStatus = cxt->current; + + cxt->current = status; + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + + cxt->current = prevStatus; + break; + } + + case jpiVariable: + { + int32 len; + const char *name = jspGetString(jpi, &len); + ListCell *lc1; + ListCell *lc2; + + Assert(status == jpdsNonDateTime); + + forboth(lc1, cxt->varnames, lc2, cxt->varexprs) + { + String *varname = lfirst_node(String, lc1); + Node *varexpr = lfirst(lc2); + + if (strncmp(varname->sval, name, len)) + continue; + + switch (exprType(varexpr)) + { + case DATEOID: + case TIMEOID: + case TIMESTAMPOID: + status = jpdsDateTimeNonZoned; + break; + + case TIMETZOID: + case TIMESTAMPTZOID: + status = jpdsDateTimeZoned; + break; + + default: + status = jpdsNonDateTime; + break; + } + + break; + } + break; + } + + case jpiEqual: + case jpiNotEqual: + case jpiLess: + case jpiGreater: + case jpiLessOrEqual: + case jpiGreaterOrEqual: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + leftStatus = jspIsMutableWalker(&arg, cxt); + + jspGetRightArg(jpi, &arg); + rightStatus = jspIsMutableWalker(&arg, cxt); + + /* + * Comparison of datetime type with different timezone status + * is mutable. + */ + if (leftStatus != jpdsNonDateTime && + rightStatus != jpdsNonDateTime && + (leftStatus == jpdsUnknownDateTime || + rightStatus == jpdsUnknownDateTime || + leftStatus != rightStatus)) + cxt->mutable = true; + break; + + case jpiNot: + case jpiIsUnknown: + case jpiExists: + case jpiPlus: + case jpiMinus: + Assert(status == jpdsNonDateTime); + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiAnd: + case jpiOr: + case jpiAdd: + case jpiSub: + case jpiMul: + case jpiDiv: + case jpiMod: + case jpiStartsWith: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + jspGetRightArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiIndexArray: + for (int i = 0; i < jpi->content.array.nelems; i++) + { + JsonPathItem from; + JsonPathItem to; + + if (jspGetArraySubscript(jpi, &from, &to, i)) + jspIsMutableWalker(&to, cxt); + + jspIsMutableWalker(&from, cxt); + } + /* FALLTHROUGH */ + + case jpiAnyArray: + if (!cxt->lax) + status = jpdsNonDateTime; + break; + + case jpiAny: + if (jpi->content.anybounds.first > 0) + status = jpdsNonDateTime; + break; + + case jpiDatetime: + if (jpi->content.arg) + { + char *template; + + jspGetArg(jpi, &arg); + if (arg.type != jpiString) + { + status = jpdsNonDateTime; + break; /* there will be runtime error */ + } + + template = jspGetString(&arg, NULL); + if (datetime_format_has_tz(template)) + status = jpdsDateTimeZoned; + else + status = jpdsDateTimeNonZoned; + } + else + { + status = jpdsUnknownDateTime; + } + break; + + case jpiLikeRegex: + Assert(status == jpdsNonDateTime); + jspInitByBuffer(&arg, jpi->base, jpi->content.like_regex.expr); + jspIsMutableWalker(&arg, cxt); + break; + + /* literals */ + case jpiNull: + case jpiString: + case jpiNumeric: + case jpiBool: + /* accessors */ + case jpiKey: + case jpiAnyKey: + /* special items */ + case jpiSubscript: + case jpiLast: + /* item methods */ + case jpiType: + case jpiSize: + case jpiAbs: + case jpiFloor: + case jpiCeiling: + case jpiDouble: + case jpiKeyValue: + status = jpdsNonDateTime; + break; + } + + if (!jspGetNext(jpi, &next)) + break; + + jpi = &next; + } + + return status; +} + +/* + * Check whether jsonpath expression is immutable or not. + */ +bool +jspIsMutable(JsonPath *path, List *varnames, List *varexprs) +{ + JsonPathMutableContext cxt; + JsonPathItem jpi; + + cxt.varnames = varnames; + cxt.varexprs = varexprs; + cxt.current = jpdsNonDateTime; + cxt.lax = (path->header & JSONPATH_LAX) != 0; + cxt.mutable = false; + + jspInit(&jpi, path); + jspIsMutableWalker(&jpi, &cxt); + + return cxt.mutable; +} diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index 2d0599b4aa..eded22e194 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -87,12 +87,16 @@ typedef struct JsonBaseObjectInfo int id; } JsonBaseObjectInfo; +typedef int (*JsonPathVarCallback) (void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); + /* * Context of jsonpath execution. */ typedef struct JsonPathExecContext { - Jsonb *vars; /* variables to substitute into jsonpath */ + void *vars; /* variables to substitute into jsonpath */ + JsonPathVarCallback getVar; JsonbValue *root; /* for $ evaluation */ JsonbValue *current; /* for @ evaluation */ JsonBaseObjectInfo baseObject; /* "base object" for .keyvalue() @@ -174,7 +178,8 @@ typedef JsonPathBool (*JsonPathPredicateCallback) (JsonPathItem *jsp, void *param); typedef Numeric (*BinaryArithmFunc) (Numeric num1, Numeric num2, bool *error); -static JsonPathExecResult executeJsonPath(JsonPath *path, Jsonb *vars, +static JsonPathExecResult executeJsonPath(JsonPath *path, void *vars, + JsonPathVarCallback getVar, Jsonb *json, bool throwErrors, JsonValueList *result, bool useTz); static JsonPathExecResult executeItem(JsonPathExecContext *cxt, @@ -225,8 +230,13 @@ static JsonPathExecResult appendBoolResult(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonValueList *found, JsonPathBool res); static void getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, JsonbValue *value); +static int GetJsonPathVar(void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); static void getJsonPathVariable(JsonPathExecContext *cxt, - JsonPathItem *variable, Jsonb *vars, JsonbValue *value); + JsonPathItem *variable, JsonbValue *value); +static int getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, + int varNameLen, JsonbValue *val, + JsonbValue *baseObject); static int JsonbArraySize(JsonbValue *jb); static JsonPathBool executeComparison(JsonPathItem *cmp, JsonbValue *lv, JsonbValue *rv, void *p); @@ -284,7 +294,8 @@ jsonb_path_exists_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - res = executeJsonPath(jp, vars, jb, !silent, NULL, tz); + res = executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, NULL, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -339,7 +350,8 @@ jsonb_path_match_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -417,7 +429,8 @@ jsonb_path_query_internal(FunctionCallInfo fcinfo, bool tz) vars = PG_GETARG_JSONB_P_COPY(2); silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); funcctx->user_fctx = JsonValueListGetList(&found); @@ -464,7 +477,8 @@ jsonb_path_query_array_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_RETURN_JSONB_P(JsonbValueToJsonb(wrapItemsInArray(&found))); } @@ -495,7 +509,8 @@ jsonb_path_query_first_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); if (JsonValueListLength(&found) >= 1) PG_RETURN_JSONB_P(JsonbValueToJsonb(JsonValueListHead(&found))); @@ -537,8 +552,9 @@ jsonb_path_query_first_tz(PG_FUNCTION_ARGS) * In other case it tries to find all the satisfied result items. */ static JsonPathExecResult -executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, - JsonValueList *result, bool useTz) +executeJsonPath(JsonPath *path, void *vars, JsonPathVarCallback getVar, + Jsonb *json, bool throwErrors, JsonValueList *result, + bool useTz) { JsonPathExecContext cxt; JsonPathExecResult res; @@ -550,22 +566,16 @@ executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, if (!JsonbExtractScalar(&json->root, &jbv)) JsonbInitBinary(&jbv, json); - if (vars && !JsonContainerIsObject(&vars->root)) - { - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"vars\" argument is not an object"), - errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); - } - cxt.vars = vars; + cxt.getVar = getVar; cxt.laxMode = (path->header & JSONPATH_LAX) != 0; cxt.ignoreStructuralErrors = cxt.laxMode; cxt.root = &jbv; cxt.current = &jbv; cxt.baseObject.jbc = NULL; cxt.baseObject.id = 0; - cxt.lastGeneratedObjectId = vars ? 2 : 1; + /* 1 + number of base objects in vars */ + cxt.lastGeneratedObjectId = 1 + getVar(vars, NULL, 0, NULL, NULL); cxt.innermostArraySize = -1; cxt.throwErrors = throwErrors; cxt.useTz = useTz; @@ -2108,54 +2118,118 @@ getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, &value->val.string.len); break; case jpiVariable: - getJsonPathVariable(cxt, item, cxt->vars, value); + getJsonPathVariable(cxt, item, value); return; default: elog(ERROR, "unexpected jsonpath item type"); } } +/* + * Returns the computed value of a JSON path variable with given name. + */ +static int +GetJsonPathVar(void *cxt, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject) +{ + JsonPathVariable *var = NULL; + List *vars = cxt; + ListCell *lc; + int id = 1; + + if (!varName) + return list_length(vars); + + foreach(lc, vars) + { + JsonPathVariable *curvar = lfirst(lc); + + if (!strncmp(curvar->name, varName, varNameLen)) + { + var = curvar; + break; + } + + id++; + } + + if (!var) + return -1; + + if (var->isnull) + { + val->type = jbvNull; + return 0; + } + + JsonItemFromDatum(var->value, var->typid, var->typmod, val); + + *baseObject = *val; + return id; +} + /* * Get the value of variable passed to jsonpath executor */ static void getJsonPathVariable(JsonPathExecContext *cxt, JsonPathItem *variable, - Jsonb *vars, JsonbValue *value) + JsonbValue *value) { char *varName; int varNameLength; + JsonbValue baseObject; + int baseObjectId; + + Assert(variable->type == jpiVariable); + varName = jspGetString(variable, &varNameLength); + + if (!cxt->vars || + (baseObjectId = cxt->getVar(cxt->vars, varName, varNameLength, value, + &baseObject)) < 0) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("could not find jsonpath variable \"%s\"", + pnstrdup(varName, varNameLength)))); + + if (baseObjectId > 0) + setBaseObject(cxt, &baseObject, baseObjectId); +} + +static int +getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, int varNameLength, + JsonbValue *value, JsonbValue *baseObject) +{ + Jsonb *vars = varsJsonb; JsonbValue tmp; JsonbValue *v; - if (!vars) + if (!varName) { - value->type = jbvNull; - return; + if (vars && !JsonContainerIsObject(&vars->root)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"vars\" argument is not an object"), + errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); + } + + return vars ? 1 : 0; /* count of base objects */ } - Assert(variable->type == jpiVariable); - varName = jspGetString(variable, &varNameLength); tmp.type = jbvString; tmp.val.string.val = varName; tmp.val.string.len = varNameLength; v = findJsonbValueFromContainer(&vars->root, JB_FOBJECT, &tmp); - if (v) - { - *value = *v; - pfree(v); - } - else - { - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("could not find jsonpath variable \"%s\"", - pnstrdup(varName, varNameLength)))); - } + if (!v) + return -1; - JsonbInitBinary(&tmp, vars); - setBaseObject(cxt, &tmp, 1); + *value = *v; + pfree(v); + + JsonbInitBinary(baseObject, vars); + return 1; } /**************** Support functions for JsonPath execution *****************/ @@ -2812,3 +2886,240 @@ compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, return DatumGetInt32(DirectFunctionCall2(cmpfunc, val1, val2)); } + +/* Executor-callable JSON_EXISTS implementation */ +bool +JsonPathExists(Datum jb, JsonPath *jp, List *vars, bool *error) +{ + JsonPathExecResult res = executeJsonPath(jp, vars, GetJsonPathVar, + DatumGetJsonbP(jb), !error, NULL, + true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + *error = true; + + return res == jperOk; +} + +/* Executor-callable JSON_QUERY implementation */ +Datum +JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, bool *empty, + bool *error, List *vars) +{ + JsonbValue *first; + bool wrap; + JsonValueList found = {0}; + JsonPathExecResult res PG_USED_FOR_ASSERTS_ONLY; + int count; + + res = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + { + *error = true; + *empty = false; + return (Datum) 0; + } + + count = JsonValueListLength(&found); + + first = count ? JsonValueListHead(&found) : NULL; + + if (!first) + wrap = false; + else if (wrapper == JSW_NONE) + wrap = false; + else if (wrapper == JSW_UNCONDITIONAL) + wrap = true; + else if (wrapper == JSW_CONDITIONAL) + wrap = count > 1 || + IsAJsonbScalar(first) || + (first->type == jbvBinary && + JsonContainerIsScalar(first->val.binary.data)); + else + { + elog(ERROR, "unrecognized json wrapper %d", wrapper); + wrap = false; + } + + if (wrap) + return JsonbPGetDatum(JsonbValueToJsonb(wrapItemsInArray(&found))); + + if (count > 1) + { + if (error) + { + *error = true; + return (Datum) 0; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_QUERY should return singleton item without wrapper"), + errhint("Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array."))); + } + + if (first) + return JsonbPGetDatum(JsonbValueToJsonb(first)); + + *empty = true; + return PointerGetDatum(NULL); +} + +/* Executor-callable JSON_VALUE implementation */ +JsonbValue * +JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars) +{ + JsonbValue *res; + JsonValueList found = {0}; + JsonPathExecResult jper PG_USED_FOR_ASSERTS_ONLY; + int count; + + jper = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(jper)); + + if (error && jperIsError(jper)) + { + *error = true; + *empty = false; + return NULL; + } + + count = JsonValueListLength(&found); + + *empty = !count; + + if (*empty) + return NULL; + + if (count > 1) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + res = JsonValueListHead(&found); + + if (res->type == jbvBinary && + JsonContainerIsScalar(res->val.binary.data)) + JsonbExtractScalar(res->val.binary.data, res); + + if (!IsAJsonbScalar(res)) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_SCALAR_REQUIRED), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + if (res->type == jbvNull) + return NULL; + + return res; +} + +static void +JsonbValueInitNumericDatum(JsonbValue *jbv, Datum num) +{ + jbv->type = jbvNumeric; + jbv->val.numeric = DatumGetNumeric(num); +} + +void +JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) +{ + switch (typid) + { + case BOOLOID: + res->type = jbvBool; + res->val.boolean = DatumGetBool(val); + break; + case NUMERICOID: + JsonbValueInitNumericDatum(res, val); + break; + case INT2OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int2_numeric, val)); + break; + case INT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int4_numeric, val)); + break; + case INT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int8_numeric, val)); + break; + case FLOAT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float4_numeric, val)); + break; + case FLOAT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float8_numeric, val)); + break; + case TEXTOID: + case VARCHAROID: + res->type = jbvString; + res->val.string.val = VARDATA_ANY(val); + res->val.string.len = VARSIZE_ANY_EXHDR(val); + break; + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + res->type = jbvDatetime; + res->val.datetime.value = val; + res->val.datetime.typid = typid; + res->val.datetime.typmod = typmod; + res->val.datetime.tz = 0; + break; + case JSONBOID: + { + JsonbValue *jbv = res; + Jsonb *jb = DatumGetJsonbP(val); + + if (JsonContainerIsScalar(&jb->root)) + { + bool result PG_USED_FOR_ASSERTS_ONLY; + + result = JsonbExtractScalar(&jb->root, jbv); + Assert(result); + } + else + JsonbInitBinary(jbv, jb); + break; + } + case JSONOID: + { + text *txt = DatumGetTextP(val); + char *str = text_to_cstring(txt); + Jsonb *jb; + + jb = DatumGetJsonbP(DirectFunctionCall1(jsonb_in, + CStringGetDatum(str))); + pfree(str); + + JsonItemFromDatum(JsonbPGetDatum(jb), JSONBOID, -1, res); + break; + } + default: + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); + } +} diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d1b03d6cb2..d1ec418ccf 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -476,6 +476,8 @@ static void get_const_expr(Const *constval, deparse_context *context, int showtype); static void get_const_collation(Const *constval, deparse_context *context); static void get_json_format(JsonFormat *format, StringInfo buf); +static void get_json_returning(JsonReturning *returning, StringInfo buf, + bool json_format_by_default); static void get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, bool showimplicit); static void get_json_constructor_options(JsonConstructorExpr *ctor, @@ -518,6 +520,8 @@ static char *generate_qualified_type_name(Oid typid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); +static void get_json_path_spec(Node *path_spec, deparse_context *context, + bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8279,6 +8283,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_WindowFunc: case T_FuncExpr: case T_JsonConstructorExpr: + case T_JsonExpr: /* function-like: name(..) or name[..] */ return true; @@ -8450,6 +8455,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_GroupingFunc: /* own parentheses */ case T_WindowFunc: /* own parentheses */ case T_CaseExpr: /* other separators */ + case T_JsonExpr: /* own parentheses */ return true; default: return false; @@ -8565,6 +8571,65 @@ get_rule_expr_paren(Node *node, deparse_context *context, appendStringInfoChar(context->buf, ')'); } +static void +get_json_behavior(JsonBehavior *behavior, deparse_context *context, + const char *on) +{ + /* + * The order of array elements must correspond to the order of + * JsonBehaviorType members. + */ + const char *behavior_names[] = + { + " NULL", + " ERROR", + " EMPTY", + " TRUE", + " FALSE", + " UNKNOWN", + " EMPTY ARRAY", + " EMPTY OBJECT", + " DEFAULT " + }; + + if ((int) behavior->btype < 0 || behavior->btype >= lengthof(behavior_names)) + elog(ERROR, "invalid json behavior type: %d", behavior->btype); + + appendStringInfoString(context->buf, behavior_names[behavior->btype]); + + if (behavior->btype == JSON_BEHAVIOR_DEFAULT) + get_rule_expr(behavior->default_expr, context, false); + + appendStringInfo(context->buf, " ON %s", on); +} + +/* + * get_json_expr_options + * + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + */ +static void +get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, + JsonBehaviorType default_behavior) +{ + if (jsexpr->op == JSON_QUERY_OP) + { + if (jsexpr->wrapper == JSW_CONDITIONAL) + appendStringInfo(context->buf, " WITH CONDITIONAL WRAPPER"); + else if (jsexpr->wrapper == JSW_UNCONDITIONAL) + appendStringInfo(context->buf, " WITH UNCONDITIONAL WRAPPER"); + + if (jsexpr->omit_quotes) + appendStringInfo(context->buf, " OMIT QUOTES"); + } + + if (jsexpr->op != JSON_EXISTS_OP && + jsexpr->on_empty->btype != default_behavior) + get_json_behavior(jsexpr->on_empty, context, "EMPTY"); + + if (jsexpr->on_error->btype != default_behavior) + get_json_behavior(jsexpr->on_error, context, "ERROR"); +} /* ---------- * get_rule_expr - Parse back an expression @@ -9724,6 +9789,7 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonValueExpr: { JsonValueExpr *jve = (JsonValueExpr *) node; @@ -9773,6 +9839,63 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + appendStringInfoString(buf, "JSON_QUERY("); + break; + case JSON_VALUE_OP: + appendStringInfoString(buf, "JSON_VALUE("); + break; + case JSON_EXISTS_OP: + appendStringInfoString(buf, "JSON_EXISTS("); + break; + } + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_json_path_spec(jexpr->path_spec, context, showimplicit); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoString(buf, " PASSING "); + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + get_rule_expr((Node *) lfirst(lc2), context, showimplicit); + appendStringInfo(buf, " AS %s", + ((String *) lfirst_node(String, lc1))->sval); + } + } + + if (jexpr->op != JSON_EXISTS_OP || + jexpr->returning->typid != BOOLOID) + get_json_returning(jexpr->returning, context->buf, + jexpr->op == JSON_QUERY_OP); + + get_json_expr_options(jexpr, context, + jexpr->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : JSON_BEHAVIOR_NULL); + + appendStringInfoString(buf, ")"); + } + break; + case T_List: { char *sep; @@ -9896,6 +10019,7 @@ looks_like_function(Node *node) case T_MinMaxExpr: case T_SQLValueFunction: case T_XmlExpr: + case T_JsonExpr: /* these are all accepted by func_expr_common_subexpr */ return true; default: @@ -10755,6 +10879,18 @@ get_const_collation(Const *constval, deparse_context *context) } } +/* + * get_json_path_spec - Parse back a JSON path specification + */ +static void +get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit) +{ + if (IsA(path_spec, Const)) + get_const_expr((Const *) path_spec, context, -1); + else + get_rule_expr(path_spec, context, showimplicit); +} + /* * get_json_format - Parse back a JsonFormat node */ diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index 048573c2bc..69fb577850 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -16,12 +16,15 @@ #include "executor/nodeAgg.h" #include "nodes/execnodes.h" +#include "nodes/miscnodes.h" /* forward references to avoid circularity */ struct ExprEvalStep; struct SubscriptingRefState; struct ScalarArrayOpExprHashTable; struct JsonConstructorExprState; +struct JsonbValue; +struct JsonExprState; /* Bits in ExprState->flags (see also execnodes.h for public flag bits): */ /* expression's interpreter has been initialized */ @@ -238,6 +241,11 @@ typedef enum ExprEvalOp EEOP_XMLEXPR, EEOP_JSON_CONSTRUCTOR, EEOP_IS_JSON, + EEOP_JSONEXPR_SKIP, + EEOP_JSONEXPR_PATH, + EEOP_JSONEXPR_BEHAVIOR, + EEOP_JSONEXPR_COERCION, + EEOP_JSONEXPR_COERCION_FINISH, EEOP_AGGREF, EEOP_GROUPING_FUNC, EEOP_WINDOW_FUNC, @@ -689,6 +697,57 @@ typedef struct ExprEvalStep JsonIsPredicate *pred; /* original expression node */ } is_json; + /* for EEOP_JSONEXPR_PATH */ + struct + { + struct JsonExprState *jsestate; + } jsonexpr; + + /* for EEOP_JSONEXPR_SKIP */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprSkip() */ + int jump_coercion; + int jump_passing_args; + } jsonexpr_skip; + + /* for EEOP_JSONEXPR_BEHAVIOR */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprBehavior() */ + int jump_onerror_expr; + int jump_onempty_expr; + int jump_coercion; + int jump_skip_coercion; + } jsonexpr_behavior; + + /* for EEOP_JSONEXPR_COERCION */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion; + + /* for EEOP_JSONEXPR_COERCION_FINISH */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion_finish; } d; } ExprEvalStep; @@ -752,6 +811,111 @@ typedef struct JsonConstructorExprState int nargs; } JsonConstructorExprState; +/* + * Information computed before evaluating a JsonExpr expression. + */ +typedef struct JsonExprPreEvalState +{ + /* value/isnull for JsonExpr.formatted_expr */ + NullableDatum formatted_expr; + + /* value/isnull for JsonExpr.pathspec */ + NullableDatum pathspec; + + /* JsonPathVariable entries for JsonExpr.passing_values */ + List *args; +} JsonExprPreEvalState; + +/* + * State for a given member of JsonItemCoercions. + */ +typedef struct JsonCoercionState +{ + /* Expression used to evaluate the coercion */ + JsonCoercion *coercion; + + /* ExprEvalStep to compute this coercion's expression */ + int jump_eval_expr; +} JsonCoercionState; + +/* + * State for evaluating the coercion for a given JSON item using one of + * the following coercions. + * + * Note that while ExecInitExprRec() for JsonItemCoercions will initialize + * ExprEvalSteps for all of the members that need it, only one will get run + * during a given evaluation of the enclosing JsonExpr depending on the type + * of the result JSON item. + */ +typedef struct JsonItemCoercionsState +{ + JsonCoercionState *null; + JsonCoercionState *string; + JsonCoercionState *numeric; + JsonCoercionState *boolean; + JsonCoercionState *date; + JsonCoercionState *time; + JsonCoercionState *timetz; + JsonCoercionState *timestamp; + JsonCoercionState *timestamptz; + JsonCoercionState *composite; +} JsonItemCoercionsState; + +/* + * State for some post-JsonExpr-evaluation processing steps that gets filled + * in JsonExpr evaluation. + */ +typedef struct JsonExprPostEvalState +{ + /* Is JSON item empty? */ + bool empty; + + /* Did JSON item evaluation cause an error? */ + bool error; + + /* Cache for json_populate_type() called for coercion in some cases */ + void *cache; + + /* + * State for evaluating a JSON item coercion. Points to one of those in + * JsonExprState.coercions chosen by ExecPrepareJsonItemCoercion(). + */ + JsonCoercionState *item_jcstate; + bool coercion_error; + bool coercion_done; + + ErrorSaveContext escontext; +} JsonExprPostEvalState; + +/* EEOP_JSONEXPR state, too big to inline */ +typedef struct JsonExprState +{ + /* original expression node */ + JsonExpr *jsexpr; + + /* + * Should errors be thrown or handled softly? Different parts of + * evaluating a given JsonExpr may require different treatment depending + * on the JsonExprOp; see ExecEvalJsonExprBehavior(). + */ + bool throw_error; + + JsonExprPreEvalState pre_eval; + JsonExprPostEvalState post_eval; + + struct + { + FmgrInfo *finfo; /* typinput function for output type */ + Oid typioparam; + } input; /* I/O info for output type */ + + /* + * Either of the following two is used by ExecEvalJsonExprCoercion() to + * apply coercion to the final result if needed. + */ + JsonCoercionState *result_jcstate; + JsonItemCoercionsState *item_jcstates; +} JsonExprState; /* functions in execExpr.c */ extern void ExprEvalPushStep(ExprState *es, const ExprEvalStep *s); @@ -805,6 +969,14 @@ extern void ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op); extern void ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull); +extern int ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op); +extern void ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, + ExprContext *econtext); extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op); extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op, ExprContext *econtext); diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index ac02247947..089d1c5750 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -17,6 +17,7 @@ #include "executor/execdesc.h" #include "fmgr.h" #include "nodes/lockoptions.h" +#include "nodes/miscnodes.h" #include "nodes/parsenodes.h" #include "utils/memutils.h" diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index cb714f4a19..584bf7001a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -129,6 +129,9 @@ typedef struct ExprState Datum *innermost_domainval; bool *innermost_domainnull; + + /* ErrorSaveContext for soft-error capture. */ + Node *escontext; } ExprState; diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 06d991b725..5e149b0266 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -111,6 +111,7 @@ extern VacuumRelation *makeVacuumRelation(RangeVar *relation, Oid oid, List *va_ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *expr, JsonFormat *format); +extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index a495a814fd..d4d8445312 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1726,6 +1726,12 @@ typedef enum JsonQuotes JS_QUOTES_OMIT /* OMIT QUOTES */ } JsonQuotes; +/* + * JsonPathSpec - + * representation of JSON path constant + */ +typedef char *JsonPathSpec; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1737,6 +1743,48 @@ typedef struct JsonOutput JsonReturning *returning; /* RETURNING FORMAT clause and type Oids */ } JsonOutput; +/* + * JsonArgument - + * representation of argument from JSON PASSING clause + */ +typedef struct JsonArgument +{ + NodeTag type; + JsonValueExpr *val; /* argument value expression */ + char *name; /* argument name */ +} JsonArgument; + +/* + * JsonCommon - + * representation of common syntax of functions using JSON path + */ +typedef struct JsonCommon +{ + NodeTag type; + JsonValueExpr *expr; /* context item expression */ + Node *pathspec; /* JSON path specification expression */ + char *pathname; /* path name, if any */ + List *passing; /* list of PASSING clause arguments, if any */ + int location; /* token location, or -1 if unknown */ +} JsonCommon; + +/* + * JsonFuncExpr - + * untransformed representation of JSON function expressions + */ +typedef struct JsonFuncExpr +{ + NodeTag type; + JsonExprOp op; /* expression type */ + JsonCommon *common; /* common syntax */ + JsonOutput *output; /* output clause, if specified */ + JsonBehavior *on_empty; /* ON EMPTY behavior, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + JsonWrapper wrapper; /* array wrapper behavior (JSON_QUERY only) */ + bool omit_quotes; /* omit or keep quotes? (JSON_QUERY only) */ + int location; /* token location, or -1 if unknown */ +} JsonFuncExpr; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6c6a1810af..0e5a160c90 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1542,6 +1542,17 @@ typedef struct XmlExpr int location; } XmlExpr; +/* + * JsonExprOp - + * enumeration of JSON functions using JSON path + */ +typedef enum JsonExprOp +{ + JSON_VALUE_OP, /* JSON_VALUE() */ + JSON_QUERY_OP, /* JSON_QUERY() */ + JSON_EXISTS_OP /* JSON_EXISTS() */ +} JsonExprOp; + /* * JsonEncoding - * representation of JSON ENCODING clause @@ -1566,6 +1577,37 @@ typedef enum JsonFormatType * jsonb */ } JsonFormatType; +/* + * JsonBehaviorType - + * enumeration of behavior types used in JSON ON ... BEHAVIOR clause + * + * If enum members are reordered, get_json_behavior() from ruleutils.c + * must be updated accordingly. + */ +typedef enum JsonBehaviorType +{ + JSON_BEHAVIOR_NULL = 0, + JSON_BEHAVIOR_ERROR, + JSON_BEHAVIOR_EMPTY, + JSON_BEHAVIOR_TRUE, + JSON_BEHAVIOR_FALSE, + JSON_BEHAVIOR_UNKNOWN, + JSON_BEHAVIOR_EMPTY_ARRAY, + JSON_BEHAVIOR_EMPTY_OBJECT, + JSON_BEHAVIOR_DEFAULT +} JsonBehaviorType; + +/* + * JsonWrapper - + * representation of WRAPPER clause for JSON_QUERY() + */ +typedef enum JsonWrapper +{ + JSW_NONE, + JSW_CONDITIONAL, + JSW_UNCONDITIONAL, +} JsonWrapper; + /* * JsonFormat - * representation of JSON FORMAT clause @@ -1656,6 +1698,73 @@ typedef struct JsonIsPredicate int location; /* token location, or -1 if unknown */ } JsonIsPredicate; +/* + * JsonBehavior - + * representation of JSON ON ... BEHAVIOR clause + */ +typedef struct JsonBehavior +{ + NodeTag type; + JsonBehaviorType btype; /* behavior type */ + Node *default_expr; /* default expression, if any */ +} JsonBehavior; + +/* + * JsonCoercion - + * coercion from SQL/JSON item types to SQL types + */ +typedef struct JsonCoercion +{ + NodeTag type; + Node *expr; /* resulting expression coerced to target type */ + bool via_populate; /* coerce result using json_populate_type()? */ + bool via_io; /* coerce result using type input function? */ + Oid collation; /* collation for coercion via I/O or populate */ +} JsonCoercion; + +/* + * JsonItemCoercions - + * expressions for coercion from SQL/JSON item types directly to the + * output SQL type + */ +typedef struct JsonItemCoercions +{ + NodeTag type; + JsonCoercion *null; + JsonCoercion *string; + JsonCoercion *numeric; + JsonCoercion *boolean; + JsonCoercion *date; + JsonCoercion *time; + JsonCoercion *timetz; + JsonCoercion *timestamp; + JsonCoercion *timestamptz; + JsonCoercion *composite; /* arrays and objects */ +} JsonItemCoercions; + +/* + * JsonExpr - + * transformed representation of JSON_VALUE(), JSON_QUERY(), JSON_EXISTS() + */ +typedef struct JsonExpr +{ + Expr xpr; + JsonExprOp op; /* json function ID */ + Node *formatted_expr; /* formatted context item expression */ + JsonCoercion *result_coercion; /* resulting coercion to RETURNING type */ + JsonFormat *format; /* context item format (JSON/JSONB) */ + Node *path_spec; /* JSON path specification expression */ + List *passing_names; /* PASSING argument names */ + List *passing_values; /* PASSING argument values */ + JsonReturning *returning; /* RETURNING clause type/format info */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + JsonItemCoercions *coercions; /* coercions for JSON_VALUE */ + JsonWrapper wrapper; /* WRAPPER for JSON_QUERY */ + bool omit_quotes; /* KEEP/OMIT QUOTES for JSON_QUERY */ + int location; /* token location, or -1 if unknown */ +} JsonExpr; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 5984dcfa4b..0954d9fc7b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -93,6 +93,7 @@ PG_KEYWORD("commit", COMMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("committed", COMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("compression", COMPRESSION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("concurrently", CONCURRENTLY, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("conditional", CONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("configuration", CONFIGURATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("conflict", CONFLICT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("connection", CONNECTION, UNRESERVED_KEYWORD, BARE_LABEL) @@ -147,11 +148,13 @@ PG_KEYWORD("double", DOUBLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("drop", DROP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("each", EACH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("empty", EMPTY_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("event", EVENT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("except", EXCEPT, RESERVED_KEYWORD, AS_LABEL) @@ -233,10 +236,14 @@ PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_exists", JSON_EXISTS, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) @@ -302,6 +309,7 @@ PG_KEYWORD("off", OFF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("offset", OFFSET, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("oids", OIDS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("old", OLD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("omit", OMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("on", ON, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("only", ONLY, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("operator", OPERATOR, UNRESERVED_KEYWORD, BARE_LABEL) @@ -344,6 +352,7 @@ PG_KEYWORD("procedures", PROCEDURES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("publication", PUBLICATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("quotes", QUOTES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("range", RANGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("read", READ, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("real", REAL, COL_NAME_KEYWORD, BARE_LABEL) @@ -414,6 +423,7 @@ PG_KEYWORD("stdout", STDOUT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("storage", STORAGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("stored", STORED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("string", STRING_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("subscription", SUBSCRIPTION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("substring", SUBSTRING, COL_NAME_KEYWORD, BARE_LABEL) @@ -449,6 +459,7 @@ PG_KEYWORD("types", TYPES_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uescape", UESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unbounded", UNBOUNDED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uncommitted", UNCOMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("unconditional", UNCONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unencrypted", UNENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("union", UNION, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("unique", UNIQUE, RESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/formatting.h b/src/include/utils/formatting.h index 0cad3a2709..d4ca0f42ff 100644 --- a/src/include/utils/formatting.h +++ b/src/include/utils/formatting.h @@ -17,7 +17,6 @@ #ifndef _FORMATTING_H_ #define _FORMATTING_H_ - extern char *str_tolower(const char *buff, size_t nbytes, Oid collid); extern char *str_toupper(const char *buff, size_t nbytes, Oid collid); extern char *str_initcap(const char *buff, size_t nbytes, Oid collid); @@ -29,5 +28,6 @@ extern char *asc_initcap(const char *buff, size_t nbytes); extern Datum parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, Oid *typid, int32 *typmod, int *tz, struct Node *escontext); +extern bool datetime_format_has_tz(const char *fmt_str); #endif diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index 8417a74298..d9e28d14ce 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -439,6 +439,9 @@ extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, int estimated_len); +extern Jsonb *JsonbMakeEmptyArray(void); +extern Jsonb *JsonbMakeEmptyObject(void); +extern char *JsonbUnquote(Jsonb *jb); extern bool JsonbExtractScalar(JsonbContainer *jbc, JsonbValue *res); extern const char *JsonbTypeName(JsonbValue *val); diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index a85203d4a4..cd9cc1480e 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -15,6 +15,7 @@ #define JSONFUNCS_H #include "common/jsonapi.h" +#include "nodes/nodes.h" #include "utils/jsonb.h" /* @@ -63,4 +64,9 @@ extern Jsonb *transform_jsonb_string_values(Jsonb *jsonb, void *action_state, extern text *transform_json_string_values(text *json, void *action_state, JsonTransformStringValuesAction transform_action); +extern Datum json_populate_type(Datum json_val, Oid json_type, + Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext); + #endif diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index f0181e045f..4dae78a98c 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -16,7 +16,9 @@ #include "fmgr.h" #include "nodes/pg_list.h" +#include "nodes/primnodes.h" #include "utils/jsonb.h" +#include "utils/jsonfuncs.h" typedef struct { @@ -184,6 +186,7 @@ extern bool jspGetBool(JsonPathItem *v); extern char *jspGetString(JsonPathItem *v, int32 *len); extern bool jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, int i); +extern bool jspIsMutable(JsonPath *path, List *varnames, List *varexprs); extern const char *jspOperationName(JsonPathItemType type); @@ -261,4 +264,28 @@ extern bool jspConvertRegexFlags(uint32 xflags, int *result, struct Node *escontext); +/* + * Evaluation of jsonpath + */ + +/* External variable passed into jsonpath. */ +typedef struct JsonPathVariable +{ + char *name; + Oid typid; + int32 typmod; + Datum value; + bool isnull; +} JsonPathVariable; + +/* SQL/JSON item */ +extern void JsonItemFromDatum(Datum val, Oid typid, int32 typmod, + JsonbValue *res); + +extern bool JsonPathExists(Datum jb, JsonPath *path, List *vars, bool *error); +extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, + bool *empty, bool *error, List *vars); +extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, + bool *error, List *vars); + #endif diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer index 435c139ec2..b2aa44f36d 100644 --- a/src/interfaces/ecpg/preproc/ecpg.trailer +++ b/src/interfaces/ecpg/preproc/ecpg.trailer @@ -651,6 +651,34 @@ var_type: simple_type $$.type_index = mm_strdup("-1"); $$.type_sizeof = NULL; } + | STRING_P + { + if (INFORMIX_MODE) + { + /* In Informix mode, "string" is automatically a typedef */ + $$.type_enum = ECPGt_string; + $$.type_str = mm_strdup("char"); + $$.type_dimension = mm_strdup("-1"); + $$.type_index = mm_strdup("-1"); + $$.type_sizeof = NULL; + } + else + { + /* Otherwise, legal only if user typedef'ed it */ + struct typedefs *this = get_typedef("string", false); + + $$.type_str = (this->type->type_enum == ECPGt_varchar || this->type->type_enum == ECPGt_bytea) ? EMPTY : mm_strdup(this->name); + $$.type_enum = this->type->type_enum; + $$.type_dimension = this->type->type_dimension; + $$.type_index = this->type->type_index; + if (this->type->type_sizeof && strlen(this->type->type_sizeof) != 0) + $$.type_sizeof = this->type->type_sizeof; + else + $$.type_sizeof = cat_str(3, mm_strdup("sizeof("), mm_strdup(this->name), mm_strdup(")")); + + struct_member_list[struct_level] = ECPGstruct_member_dup(this->struct_member_list); + } + } | INTERVAL ecpg_interval { $$.type_enum = ECPGt_interval; diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out new file mode 100644 index 0000000000..8b87580752 --- /dev/null +++ b/src/test/regress/expected/json_sqljson.out @@ -0,0 +1,18 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); +ERROR: JSON_EXISTS() is not yet implemented for the json type +LINE 1: SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_VALUE +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); +ERROR: JSON_VALUE() is not yet implemented for the json type +LINE 1: SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_QUERY +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); +ERROR: JSON_QUERY() is not yet implemented for the json type +LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out new file mode 100644 index 0000000000..7eb8faf487 --- /dev/null +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -0,0 +1,1034 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL::jsonb, '$'); + json_exists +------------- + +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb 'null', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_EXISTS(jsonb 'null', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{}', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + json_exists +------------- + f +(1 row) + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + json_exists +------------- + t +(1 row) + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); + json_exists +------------- + 1 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); + json_exists +------------- + 0 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); + json_exists +------------- + true +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +ERROR: cannot cast type boolean to jsonb +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +ERROR: cannot cast type boolean to real +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); + ^ +-- JSON_VALUE +SELECT JSON_VALUE(NULL::jsonb, '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$'); + json_value +------------ + true +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + json_value +------------ + t +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); + json_value +------------ + 123 +(1 row) + +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); +ERROR: SQL/JSON item cannot be cast to target type +SELECT JSON_VALUE(jsonb '1.23', '$'); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "1.23" +SELECT JSON_VALUE(jsonb '"aaa"', '$'); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); + json_value +------------ + aa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "aaa" +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); + json_value +------------ + 111 +(1 row) + +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + ?column? +------------ + 03-01-2017 +(1 row) + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '[]', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '{}', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '1', '$.a'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); + json_value +------------ + error +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 3 +(1 row) + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); + json_value +------------ + 0 +(1 row) + +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: " " +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 5 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 1 +(1 row) + +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + x | y +---+---- + 0 | -2 + 1 | 2 + 2 | -1 +(3 rows) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); + json_value +------------ + (1,2) +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + json_value +------------ + (1,2) +(1 row) + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); + json_value +-------------------------- + Tue Feb 20 18:34:56 2018 +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- JSON_QUERY +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + json_query | json_query | json_query | json_query | json_query +--------------------+--------------------+--------------------+----------------------+---------------------- + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, null, "2"] | [1, null, "2"] | [1, null, "2"] | [[1, null, "2"]] | [[1, null, "2"]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] +(6 rows) + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + unspec | without | with cond | with uncond | with +--------------------+--------------------+---------------------+----------------------+---------------------- + | | | | + | | | | + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, 2, 3] | [1, 2, 3] | [1, 2, 3] | [[1, 2, 3]] | [[1, 2, 3]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] + | | [1, "2", null, [3]] | [1, "2", null, [3]] | [1, "2", null, [3]] +(9 rows) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + json_query +------------ + \x616161 +(1 row) + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTE... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTE... + ^ +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_QUERY should return singleton item without wrapper +HINT: Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array. +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); + json_query +------------ + [1, +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); +ERROR: expected JSON array +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + x | y | list +---+---+-------------- + 0 | 0 | [] + 0 | 1 | [1] + 0 | 2 | [1, 2] + 0 | 3 | [1, 2, 3] + 0 | 4 | [1, 2, 3, 4] + 1 | 0 | [] + 1 | 1 | [1] + 1 | 2 | [1, 2] + 1 | 3 | [1, 2, 3] + 1 | 4 | [1, 2, 3, 4] + 2 | 0 | [] + 2 | 1 | [] + 2 | 2 | [2] + 2 | 3 | [2, 3] + 2 | 4 | [2, 3, 4] + 3 | 0 | [] + 3 | 1 | [] + 3 | 2 | [] + 3 | 3 | [3] + 3 | 4 | [3, 4] + 4 | 0 | [] + 4 | 1 | [] + 4 | 2 | [] + 4 | 3 | [] + 4 | 4 | [4] +(25 rows) + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); + json_query +----------------------------------------------------- + (1,aaa,"[1, ""2"", {}]","{""x"": [1, ""2"", {}]}",) +(1 row) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); + unnest +------------------------ + {"a": 1, "b": ["foo"]} + {"a": 2, "c": {}} + 123 +(3 rows) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); + json_query +-------------- + {1,2,NULL,3} +(1 row) + +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); + json_query +------------ + 1 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- Test constraints +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); +\d test_jsonb_constraints + Table "public.test_jsonb_constraints" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+-------------------------------------------------------------------------------- + js | text | | | + i | integer | | | + x | jsonb | | | JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +Check constraints: + "test_jsonb_constraint1" CHECK (js IS JSON) + "test_jsonb_constraint2" CHECK (JSON_EXISTS(js::jsonb, '$."a"' PASSING i + 5 AS int, i::text AS txt, ARRAY[1, 2, 3] AS arr)) + "test_jsonb_constraint3" CHECK (JSON_VALUE(js::jsonb, '$."a"' RETURNING integer DEFAULT ('12'::text || i)::integer ON EMPTY ERROR ON ERROR) > i) + "test_jsonb_constraint4" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb) + "test_jsonb_constraint5" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C")) + "test_jsonb_constraint6" CHECK (JSON_EXISTS(js::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2) + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + check_clause +-------------------------------------------------------------------------------------------------------------------------- + ((JSON_EXISTS((js)::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2)) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C"))) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb)) + ((JSON_VALUE((js)::jsonb, '$."a"' RETURNING integer DEFAULT (('12'::text || i))::integer ON EMPTY ERROR ON ERROR) > i)) + ((js IS JSON)) + (JSON_EXISTS((js)::jsonb, '$."a"' PASSING (i + 5) AS int, (i)::text AS txt, ARRAY[1, 2, 3] AS arr)) +(6 rows) + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +WHERE constraint_name LIKE 'test_jsonb_constraint%'; +ERROR: syntax error at or near "WHERE" +LINE 1: WHERE constraint_name LIKE 'test_jsonb_constraint%'; + ^ +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint1" +DETAIL: Failing row contains (, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains (1, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('[]'); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ([], null, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ({"b": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint3" +DETAIL: Failing row contains ({"a": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint5" +DETAIL: Failing row contains ({"a": 7}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint4" +DETAIL: Failing row contains ({"a": 10}, 1, [1, 2]). +DROP TABLE test_jsonb_constraints; +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index cf46fa3359..cb3230f4dd 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -103,7 +103,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo # ---------- # Another group of parallel tests (JSON related) # ---------- -test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson +test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson json_sqljson jsonb_sqljson # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql new file mode 100644 index 0000000000..4f30fa46b9 --- /dev/null +++ b/src/test/regress/sql/json_sqljson.sql @@ -0,0 +1,11 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + +-- JSON_QUERY + +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql new file mode 100644 index 0000000000..963129699b --- /dev/null +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -0,0 +1,325 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL::jsonb, '$'); + +SELECT JSON_EXISTS(jsonb '[]', '$'); +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + +SELECT JSON_EXISTS(jsonb '1', '$'); +SELECT JSON_EXISTS(jsonb 'null', '$'); +SELECT JSON_EXISTS(jsonb '[]', '$'); + +SELECT JSON_EXISTS(jsonb '1', '$.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_EXISTS(jsonb 'null', '$.a'); +SELECT JSON_EXISTS(jsonb '[]', '$.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); +SELECT JSON_EXISTS(jsonb '{}', '$.a'); +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); + + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL::jsonb, '$'); + +SELECT JSON_VALUE(jsonb 'null', '$'); +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + +SELECT JSON_VALUE(jsonb 'true', '$'); +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + +SELECT JSON_VALUE(jsonb '123', '$'); +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1.23', '$'); +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '"aaa"', '$'); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); + +SELECT JSON_VALUE(jsonb '[]', '$'); +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '{}', '$'); +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1', '$.a'); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- JSON_QUERY + +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); + +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); + +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); + +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- Test constraints + +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); + +\d test_jsonb_constraints + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; +WHERE constraint_name LIKE 'test_jsonb_constraint%'; + +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +INSERT INTO test_jsonb_constraints VALUES ('[]'); +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); + +DROP TABLE test_jsonb_constraints; + +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 2f1e8a5702..e284d9db51 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1254,14 +1254,24 @@ JsonArrayAgg JsonArrayConstructor JsonArrayQueryConstructor JsonBaseObjectInfo +JsonBehavior +JsonBehaviorType +JsonCoercion +JsonCoercionState +JsonCommon JsonConstructorExpr JsonConstructorExprState JsonConstructorType JsonEncoding +JsonExpr +JsonExprOp +JsonExprState JsonFormat JsonFormatType JsonHashEntry JsonIsPredicate +JsonItemCoercions +JsonItemCoercionsState JsonIterateStringValuesAction JsonKeyValue JsonLexContext @@ -1294,6 +1304,10 @@ JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback JsonPathString +JsonPathVarCallback +JsonPathVariable +JsonPathVariableEvalContext +JsonQuotes JsonReturning JsonSemAction JsonSerializeExpr -- 2.35.3
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-07 11:30  Peter Eisentraut <peter@eisentraut.org>
  parent: Amit Langote <amitlangote09@gmail.com>
  1 sibling, 1 reply; 300+ messages in thread

From: Peter Eisentraut @ 2023-07-07 11:30 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; pgsql-hackers; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>

On 21.06.23 10:25, Amit Langote wrote:
> I realized that the patch for the "other sql/json functions" part is
> relatively straightforward and has no dependence on the "sql/json
> query functions" part getting done first.  So I've made that one the
> 0001 patch.  The patch I posted in the last email is now 0002, though
> it only has changes related to changing the order of the patch, so I
> decided not to change the patch version marker (v1).

(I suggest you change the version number anyway, next time.  There are 
plenty of numbers available.)

The 0001 patch contains a change to 
doc/src/sgml/keywords/sql2016-02-reserved.txt, which seems 
inappropriate.  The additional keywords are already listed in the 2023 
file, and they are not SQL:2016 keywords.

Another thing, I noticed that the SQL/JSON patches in PG16 introduced 
some nonstandard indentation in gram.y.  I would like to apply the 
attached patch to straighten this out.

Attachments:

gram-json-indent.patch.nocfbot (text/plain, 1.3K) download diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 39ab7eac0d..edb6c00ece 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -645,23 +645,20 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <list> hash_partbound %type <defelt> hash_partbound_elem +%type <node> json_format_clause_opt + json_value_expr + json_output_clause_opt + json_name_and_value + json_aggregate_func +%type <list> json_name_and_value_list + json_value_expr_list + json_array_aggregate_order_by_clause_opt +%type <ival> json_encoding_clause_opt + json_predicate_type_constraint +%type <boolean> json_key_uniqueness_constraint_opt + json_object_constructor_null_clause_opt + json_array_constructor_null_clause_opt -%type <node> json_format_clause_opt - json_value_expr - json_output_clause_opt - json_name_and_value - json_aggregate_func - -%type <list> json_name_and_value_list - json_value_expr_list - json_array_aggregate_order_by_clause_opt - -%type <ival> json_encoding_clause_opt - json_predicate_type_constraint - -%type <boolean> json_key_uniqueness_constraint_opt - json_object_constructor_null_clause_opt - json_array_constructor_null_clause_opt /* * Non-keyword token types. These are hard-wired into the "flex" lexer.
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-07 11:59  Amit Langote <amitlangote09@gmail.com>
  parent: Peter Eisentraut <peter@eisentraut.org>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-07-07 11:59 UTC (permalink / raw)
  To: Peter Eisentraut <peter@eisentraut.org>; +Cc: pgsql-hackers; Alvaro Herrera <alvherre@alvh.no-ip.org>

On Fri, Jul 7, 2023 at 8:31 PM Peter Eisentraut <peter@eisentraut.org> wrote:
> On 21.06.23 10:25, Amit Langote wrote:
> > I realized that the patch for the "other sql/json functions" part is
> > relatively straightforward and has no dependence on the "sql/json
> > query functions" part getting done first.  So I've made that one the
> > 0001 patch.  The patch I posted in the last email is now 0002, though
> > it only has changes related to changing the order of the patch, so I
> > decided not to change the patch version marker (v1).
>
> (I suggest you change the version number anyway, next time.  There are
> plenty of numbers available.)

Will do. :)

> The 0001 patch contains a change to
> doc/src/sgml/keywords/sql2016-02-reserved.txt, which seems
> inappropriate.  The additional keywords are already listed in the 2023
> file, and they are not SQL:2016 keywords.

Ah, indeed.  Will remove.

> Another thing, I noticed that the SQL/JSON patches in PG16 introduced
> some nonstandard indentation in gram.y.  I would like to apply the
> attached patch to straighten this out.

Sounds fine to me.

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-07 12:19  Amit Langote <amitlangote09@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 0 replies; 300+ messages in thread

From: Amit Langote @ 2023-07-07 12:19 UTC (permalink / raw)
  To: Peter Eisentraut <peter@eisentraut.org>; +Cc: pgsql-hackers; Alvaro Herrera <alvherre@alvh.no-ip.org>

On Fri, Jul 7, 2023 at 8:59 PM Amit Langote <amitlangote09@gmail.com> wrote:
> On Fri, Jul 7, 2023 at 8:31 PM Peter Eisentraut <peter@eisentraut.org> wrote:
> > On 21.06.23 10:25, Amit Langote wrote:
> > > I realized that the patch for the "other sql/json functions" part is
> > > relatively straightforward and has no dependence on the "sql/json
> > > query functions" part getting done first.  So I've made that one the
> > > 0001 patch.  The patch I posted in the last email is now 0002, though
> > > it only has changes related to changing the order of the patch, so I
> > > decided not to change the patch version marker (v1).
> >
> > (I suggest you change the version number anyway, next time.  There are
> > plenty of numbers available.)
>
> Will do. :)

Here's v2.

0001 and 0002 are new patches for some improvements of the existing code.

In the main patches (0003~), I've mainly removed a few nonterminals in
favor of new rules in the remaining nonterminals, especially in the
JSON_TABLE patch.

I've also removed additions to sql2016-02-reserved.txt as Peter suggested.

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

Attachments: download all (6 files, .tar.gz)

v2-0002-Don-t-include-CaseTestExpr-in-JsonValueExpr.forma.patch (application/octet-stream, 2.9K) download From 7bffde1e2bc83eca317579b5e6b6124a11446347 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Fri, 7 Jul 2023 20:21:58 +0900 Subject: [PATCH v2 2/6] Don't include CaseTestExpr in JsonValueExpr.formatted_expr There doesn't appear to be any need for it. --- src/backend/executor/execExpr.c | 11 ----------- src/backend/optimizer/util/clauses.c | 5 ----- src/backend/parser/parse_expr.c | 7 ++----- 3 files changed, 2 insertions(+), 21 deletions(-) diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index e6e616865c..21f7796e63 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -2297,18 +2297,7 @@ ExecInitExprRec(Expr *node, ExprState *state, ExecInitExprRec(jve->raw_expr, state, resv, resnull); if (jve->formatted_expr) - { - Datum *innermost_caseval = state->innermost_caseval; - bool *innermost_isnull = state->innermost_casenull; - - state->innermost_caseval = resv; - state->innermost_casenull = resnull; - ExecInitExprRec(jve->formatted_expr, state, resv, resnull); - - state->innermost_caseval = innermost_caseval; - state->innermost_casenull = innermost_isnull; - } break; } diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 7f453b04f8..59d8ce789e 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -2834,15 +2834,10 @@ eval_const_expressions_mutator(Node *node, if (raw && IsA(raw, Const)) { Node *formatted; - Node *save_case_val = context->case_val; - - context->case_val = raw; formatted = eval_const_expressions_mutator((Node *) jve->formatted_expr, context); - context->case_val = save_case_val; - if (formatted && IsA(formatted, Const)) return formatted; } diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 9e8a2ac22b..f02343ef64 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -3269,11 +3269,8 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, if (format != JS_FORMAT_DEFAULT) { Oid targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; - Node *orig = makeCaseTestExpr(expr); Node *coerced; - expr = orig; - if (exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), @@ -3311,7 +3308,7 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, coerced = (Node *) fexpr; } - if (coerced == orig) + if (coerced == expr) expr = rawexpr; else { @@ -3899,7 +3896,7 @@ transformJsonParseArg(ParseState *pstate, Node *jsexpr, JsonFormat *format, { JsonValueExpr *jve; - expr = makeCaseTestExpr(raw_expr); + expr = raw_expr; expr = makeJsonByteaToTextConversion(expr, format, exprLocation(expr)); *exprtype = TEXTOID; -- 2.35.3
v2-0001-Pass-constructName-to-transformJsonValueExpr.patch (application/octet-stream, 3.0K) download From f1c39298ba2fb51daac458daf21c8d4f099fe8a5 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Fri, 7 Jul 2023 12:08:58 +0900 Subject: [PATCH v2 1/6] Pass constructName to transformJsonValueExpr() Which in turn can be passed to coerce_to_specific_type() so that it can report the name of the specific JSON_* expression being transformed. --- src/backend/parser/parse_expr.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 346fd272b6..9e8a2ac22b 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -3223,6 +3223,7 @@ makeCaseTestExpr(Node *expr) */ static Node * transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, + char *constructName, JsonFormatType default_format) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); @@ -3233,12 +3234,8 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, char typcategory; bool typispreferred; - /* - * Using JSON_VALUE here is slightly bogus: perhaps we need to be passed a - * JsonConstructorType so that we can use one of JSON_OBJECTAGG, etc. - */ if (exprType(expr) == UNKNOWNOID) - expr = coerce_to_specific_type(pstate, expr, TEXTOID, "JSON_VALUE"); + expr = coerce_to_specific_type(pstate, expr, TEXTOID, constructName); rawexpr = expr; exprtype = exprType(expr); @@ -3589,6 +3586,7 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) JsonKeyValue *kv = castNode(JsonKeyValue, lfirst(lc)); Node *key = transformExprRecurse(pstate, (Node *) kv->key); Node *val = transformJsonValueExpr(pstate, kv->value, + "JSON_OBJECT()", JS_FORMAT_DEFAULT); args = lappend(args, key); @@ -3768,7 +3766,8 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) Oid aggtype; key = transformExprRecurse(pstate, (Node *) agg->arg->key); - val = transformJsonValueExpr(pstate, agg->arg->value, JS_FORMAT_DEFAULT); + val = transformJsonValueExpr(pstate, agg->arg->value, "JSON_OBJECTAGG()", + JS_FORMAT_DEFAULT); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3824,7 +3823,8 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) Oid aggfnoid; Oid aggtype; - arg = transformJsonValueExpr(pstate, agg->arg, JS_FORMAT_DEFAULT); + arg = transformJsonValueExpr(pstate, agg->arg, "JSON_ARRAYAGG()", + JS_FORMAT_DEFAULT); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3871,6 +3871,7 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) { JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, jsval, + "JSON_ARRAY()", JS_FORMAT_DEFAULT); args = lappend(args, val); -- 2.35.3
v2-0003-SQL-JSON-functions.patch (application/octet-stream, 56.2K) download From 44dd0f14bfdc9f1e15e2d9e06b35026645b7b324 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:12:39 +0900 Subject: [PATCH v2 3/6] SQL JSON functions This Patch introduces three SQL standard JSON functions: JSON() JSON_SCALAR() JSON_SERIALIZE() JSON() produces json values from text, bytea, json or jsonb values, and has facilitites for handling duplicate keys. JSON_SCALAR() produces a json value from any scalar sql value, including json and jsonb. JSON_SERIALIZE() produces text or bytea from input which containis or represents json or jsonb; For the most part these functions don't add any significant new capabilities, but they will be of use to users wanting standard compliant JSON handling. Nikita Glukhov Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby. Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org --- doc/src/sgml/func.sgml | 71 +++++ src/backend/executor/execExpr.c | 45 +++ src/backend/executor/execExprInterp.c | 42 ++- src/backend/nodes/nodeFuncs.c | 30 ++ src/backend/parser/gram.y | 65 ++++- src/backend/parser/parse_expr.c | 201 +++++++++++++- src/backend/parser/parse_target.c | 9 + src/backend/utils/adt/format_type.c | 4 + src/backend/utils/adt/json.c | 44 ++- src/backend/utils/adt/jsonb.c | 76 +++--- src/backend/utils/adt/ruleutils.c | 14 +- src/include/nodes/parsenodes.h | 48 ++++ src/include/nodes/primnodes.h | 5 +- src/include/parser/kwlist.h | 4 +- src/include/utils/json.h | 19 ++ src/include/utils/jsonb.h | 21 ++ src/test/regress/expected/sqljson.out | 376 ++++++++++++++++++++++++++ src/test/regress/sql/sqljson.sql | 93 +++++++ src/tools/pgindent/typedefs.list | 1 + 19 files changed, 1089 insertions(+), 79 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 5a47ce4343..8de8f527a5 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16001,6 +16001,77 @@ table2-mapping <returnvalue>{"a": "1", "b": "2"}</returnvalue> </para></entry> </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json constructor</primary></indexterm> + <function>json</function> ( + <replaceable>expression</replaceable> + <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional></optional> + <optional> { <literal>WITH</literal> | <literal>WITHOUT</literal> } <literal>UNIQUE</literal> <optional> <literal>KEYS</literal> </optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>json_data_type</replaceable> </optional>) + </para> + <para> + The <replaceable>expression</replaceable> can be any text type or a + <type>bytea</type> in UTF8 encoding. If the + <replaceable>expression</replaceable> is NULL, an + <acronym>SQL</acronym> null value is returned. + If <literal>WITH UNIQUE</literal> is specified, the + <replaceable>expression</replaceable> must not contain any duplicate + object keys. + </para> + <para> + <literal>json('{"a":123, "b":[true,"foo"], "a":"bar"}')</literal> + <returnvalue>{"a":123, "b":[true,"foo"], "a":"bar"}</returnvalue> + </para> + <para> + <literal>json('{"a":123,"b":[true,"foo"],"a":"bar"}' returning jsonb)</literal> + <returnvalue>{"a": "bar", "b": [true, "foo"]}</returnvalue> + </para> + </entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json_scalar</primary></indexterm> + <function>json_scalar</function> (<replaceable>expression</replaceable>) + <optional> <literal>RETURNING</literal> <replaceable>json_data_type</replaceable> </optional>) + </para> + <para> + Returns a JSON scalar value representing + <replaceable>expression</replaceable>. + If the input is NULL, an SQL NULL is returned. If the input is a number + or a boolean value, a corresponding JSON number or boolean value is + returned. For any other value a JSON string is returned. + </para> + <para> + <literal>json_scalar(123.45)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_scalar(CURRENT_TIMESTAMP)</literal> + <returnvalue>"2022-05-10T10:51:04.62128-04:00"</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <function>json_serialize</function> ( + <replaceable>expression</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional>) + </para> + <para> + Transforms an SQL/JSON value into a character or binary string. The + <replaceable>expression</replaceable> can be of any JSON type, any + character string type, or <type>bytea</type> in UTF8 encoding. + The returned type can be any character string type or + <type>bytea</type>. The default is <type>text</type>. + </para> + <para> + <literal>json_serialize('{ "a" : 1 } ' RETURNING bytea)</literal> + <returnvalue>\x7b20226122203a2031207d20</returnvalue> + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 21f7796e63..95d9fbbc7c 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -48,6 +48,8 @@ #include "utils/array.h" #include "utils/builtins.h" #include "utils/datum.h" +#include "utils/json.h" +#include "utils/jsonb.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -2313,6 +2315,12 @@ ExecInitExprRec(Expr *node, ExprState *state, { ExecInitExprRec(ctor->func, state, resv, resnull); } + else if ((ctor->type == JSCTOR_JSON_PARSE && !ctor->unique) || + ctor->type == JSCTOR_JSON_SERIALIZE) + { + /* Use the value of the first argument as a result */ + ExecInitExprRec(linitial(args), state, resv, resnull); + } else { JsonConstructorExprState *jcstate; @@ -2351,6 +2359,43 @@ ExecInitExprRec(Expr *node, ExprState *state, argno++; } + /* prepare type cache for datum_to_json[b]() */ + if (ctor->type == JSCTOR_JSON_SCALAR) + { + bool is_jsonb = + ctor->returning->format->format_type == JS_FORMAT_JSONB; + + jcstate->arg_type_cache = + palloc(sizeof(*jcstate->arg_type_cache) * nargs); + + for (int i = 0; i < nargs; i++) + { + int category; + Oid outfuncid; + Oid typid = jcstate->arg_types[i]; + + if (is_jsonb) + { + JsonbTypeCategory jbcat; + + jsonb_categorize_type(typid, &jbcat, &outfuncid); + + category = (int) jbcat; + } + else + { + JsonTypeCategory jscat; + + json_categorize_type(typid, &jscat, &outfuncid); + + category = (int) jscat; + } + + jcstate->arg_type_cache[i].outfuncid = outfuncid; + jcstate->arg_type_cache[i].category = category; + } + } + ExprEvalPushStep(state, &scratch); } diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 851946a927..b83dd1c666 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -3992,7 +3992,7 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_values, jcstate->arg_nulls, jcstate->arg_types, - jcstate->constructor->absent_on_null); + ctor->absent_on_null); else if (ctor->type == JSCTOR_JSON_OBJECT) res = (is_jsonb ? jsonb_build_object_worker : @@ -4002,6 +4002,46 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_types, jcstate->constructor->absent_on_null, jcstate->constructor->unique); + else if (ctor->type == JSCTOR_JSON_SCALAR) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + int category = jcstate->arg_type_cache[0].category; + Oid outfuncid = jcstate->arg_type_cache[0].outfuncid; + + if (is_jsonb) + res = to_jsonb_worker(value, category, outfuncid); + else + res = to_json_worker(value, category, outfuncid); + } + } + else if (ctor->type == JSCTOR_JSON_PARSE) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + text *js = DatumGetTextP(value); + + if (is_jsonb) + res = jsonb_from_text(js, true); + else + { + (void) json_validate(js, true, true); + res = value; + } + } + } else elog(ERROR, "invalid JsonConstructorExpr type %d", ctor->type); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index c41e6bb984..dda964bd19 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -3901,6 +3901,36 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonParseExpr: + { + JsonParseExpr *jpe = (JsonParseExpr *) node; + + if (WALK(jpe->expr)) + return true; + if (WALK(jpe->output)) + return true; + } + break; + case T_JsonScalarExpr: + { + JsonScalarExpr *jse = (JsonScalarExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; + case T_JsonSerializeExpr: + { + JsonSerializeExpr *jse = (JsonSerializeExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; case T_JsonConstructorExpr: { JsonConstructorExpr *ctor = (JsonConstructorExpr *) node; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 39ab7eac0d..8f239de65c 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -566,7 +566,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <list> copy_options %type <typnam> Typename SimpleTypename ConstTypename - GenericType Numeric opt_float + GenericType Numeric opt_float JsonType Character ConstCharacter CharacterWithLength CharacterWithoutLength ConstDatetime ConstInterval @@ -648,6 +648,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> json_format_clause_opt json_value_expr + json_returning_clause_opt json_output_clause_opt json_name_and_value json_aggregate_func @@ -726,6 +727,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG + JSON_SCALAR JSON_SERIALIZE KEY KEYS @@ -13984,6 +13986,7 @@ SimpleTypename: $$->typmods = list_make2(makeIntConst(INTERVAL_FULL_RANGE, -1), makeIntConst($3, @3)); } + | JsonType { $$ = $1; } ; /* We have a separate ConstTypename to allow defaulting fixed-length @@ -14002,6 +14005,7 @@ ConstTypename: | ConstBit { $$ = $1; } | ConstCharacter { $$ = $1; } | ConstDatetime { $$ = $1; } + | JsonType { $$ = $1; } ; /* @@ -14370,6 +14374,13 @@ interval_second: } ; +JsonType: + JSON + { + $$ = SystemTypeName("json"); + $$->location = @1; + } + ; /***************************************************************************** * @@ -15628,7 +15639,37 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } - ; + | JSON '(' json_value_expr json_key_uniqueness_constraint_opt + json_returning_clause_opt ')' + { + JsonParseExpr *n = makeNode(JsonParseExpr); + + n->expr = (JsonValueExpr *) $3; + n->unique_keys = $4; + n->output = (JsonOutput *) $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SCALAR '(' a_expr json_returning_clause_opt ')' + { + JsonScalarExpr *n = makeNode(JsonScalarExpr); + + n->expr = (Expr *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SERIALIZE '(' json_value_expr json_output_clause_opt ')' + { + JsonSerializeExpr *n = makeNode(JsonSerializeExpr); + + n->expr = (JsonValueExpr *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + ; + /* * SQL/XML support @@ -16376,6 +16417,20 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; +json_returning_clause_opt: + RETURNING Typename + { + JsonOutput *n = makeNode(JsonOutput); + + n->typeName = $2; + n->returning = makeNode(JsonReturning); + n->returning->format = + makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, @2); + $$ = (Node *) n; + } + | /* EMPTY */ { $$ = NULL; } + ; + json_output_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17067,7 +17122,6 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION - | JSON | KEY | KEYS | LABEL @@ -17282,10 +17336,13 @@ col_name_keyword: | INT_P | INTEGER | INTERVAL + | JSON | JSON_ARRAY | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | LEAST | NATIONAL | NCHAR @@ -17646,6 +17703,8 @@ bare_label_keyword: | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | KEY | KEYS | LABEL diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index f02343ef64..b46e06237b 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -86,6 +86,10 @@ static Node *transformJsonArrayQueryConstructor(ParseState *pstate, static Node *transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg); static Node *transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg); static Node *transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred); +static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); +static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); +static Node *transformJsonSerializeExpr(ParseState *pstate, + JsonSerializeExpr * expr); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -337,6 +341,18 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonIsPredicate(pstate, (JsonIsPredicate *) expr); break; + case T_JsonParseExpr: + result = transformJsonParseExpr(pstate, (JsonParseExpr *) expr); + break; + + case T_JsonScalarExpr: + result = transformJsonScalarExpr(pstate, (JsonScalarExpr *) expr); + break; + + case T_JsonSerializeExpr: + result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3224,7 +3240,8 @@ makeCaseTestExpr(Node *expr) static Node * transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, char *constructName, - JsonFormatType default_format) + JsonFormatType default_format, + Oid targettype) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3266,12 +3283,14 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, else format = default_format; - if (format != JS_FORMAT_DEFAULT) + if (format != JS_FORMAT_DEFAULT || + (OidIsValid(targettype) && exprtype != targettype)) { - Oid targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; Node *coerced; + bool cast_is_needed = OidIsValid(targettype); - if (exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) + if (!cast_is_needed && + exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), errmsg(ve->format->format_type == JS_FORMAT_DEFAULT ? @@ -3287,6 +3306,9 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, exprtype = TEXTOID; } + if (!OidIsValid(targettype)) + targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; + /* Try to coerce to the target type. */ coerced = coerce_to_target_type(pstate, expr, exprtype, targettype, -1, @@ -3297,11 +3319,20 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, if (!coerced) { /* If coercion failed, use to_json()/to_jsonb() functions. */ - Oid fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; - FuncExpr *fexpr = makeFuncExpr(fnoid, targettype, - list_make1(expr), - InvalidOid, InvalidOid, - COERCE_EXPLICIT_CALL); + FuncExpr *fexpr; + Oid fnoid; + + if (cast_is_needed) /* only CAST is allowed */ + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(exprtype), + format_type_be(targettype)), + parser_errposition(pstate, location))); + + fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; + fexpr = makeFuncExpr(fnoid, targettype, list_make1(expr), + InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL); fexpr->location = location; @@ -3584,7 +3615,8 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) Node *key = transformExprRecurse(pstate, (Node *) kv->key); Node *val = transformJsonValueExpr(pstate, kv->value, "JSON_OBJECT()", - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, key); args = lappend(args, val); @@ -3764,7 +3796,7 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) key = transformExprRecurse(pstate, (Node *) agg->arg->key); val = transformJsonValueExpr(pstate, agg->arg->value, "JSON_OBJECTAGG()", - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, InvalidOid); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3821,7 +3853,7 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) Oid aggtype; arg = transformJsonValueExpr(pstate, agg->arg, "JSON_ARRAYAGG()", - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, InvalidOid); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3869,7 +3901,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, jsval, "JSON_ARRAY()", - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, val); } @@ -3952,3 +3985,145 @@ transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred) return makeJsonIsPredicate(expr, NULL, pred->item_type, pred->unique_keys, pred->location); } + +/* + * Transform the output clause of a JSON_*() expression if there is one and + * create one if not. + */ +static JsonReturning * +transformJsonReturning(ParseState *pstate, JsonOutput *output, const char *fname) +{ + JsonReturning *returning; + + if (output) + { + returning = transformJsonOutput(pstate, output, false); + + Assert(OidIsValid(returning->typid)); + + if (returning->typid != JSONOID && returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), fname), + parser_errposition(pstate, output->typeName->location))); + } + else + { + /* Output type is JSON by default. */ + Oid targettype = JSONOID; + JsonFormatType format = JS_FORMAT_JSON; + + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(format, JS_ENC_DEFAULT, -1); + returning->typid = targettype; + returning->typmod = -1; + } + + return returning; +} + +/* + * Transform a JSON() expression. + */ +static Node * +transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) +{ + JsonReturning *returning = transformJsonReturning(pstate, jsexpr->output, + "JSON()"); + Node *arg; + + if (jsexpr->unique_keys) + { + /* + * Coerce string argument to text and then to json[b] in the executor + * node with key uniqueness check. + */ + JsonValueExpr *jve = jsexpr->expr; + Oid arg_type; + + arg = transformJsonParseArg(pstate, (Node *) jve->raw_expr, jve->format, + &arg_type); + + if (arg_type != TEXTOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use non-string types with WITH UNIQUE KEYS clause"), + parser_errposition(pstate, jsexpr->location))); + } + else + { + /* + * Coerce argument to target type using CAST for compatibility with PG + * function-like CASTs. + */ + arg = transformJsonValueExpr(pstate, jsexpr->expr, "JSON()", + JS_FORMAT_JSON, returning->typid); + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, + returning, jsexpr->unique_keys, false, + jsexpr->location); +} + +/* + * Transform a JSON_SCALAR() expression. + */ +static Node * +transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *jsexpr) +{ + Node *arg = transformExprRecurse(pstate, (Node *) jsexpr->expr); + JsonReturning *returning = transformJsonReturning(pstate, jsexpr->output, + "JSON_SCALAR()"); + + if (exprType(arg) == UNKNOWNOID) + arg = coerce_to_specific_type(pstate, arg, TEXTOID, "JSON_SCALAR"); + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SCALAR, list_make1(arg), NULL, + returning, false, false, jsexpr->location); +} + +/* + * Transform a JSON_SERIALIZE() expression. + */ +static Node * +transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) +{ + Node *arg = transformJsonValueExpr(pstate, expr->expr, + "JSON_SERIALIZE()", + JS_FORMAT_JSON, + InvalidOid); + JsonReturning *returning; + + if (expr->output) + { + returning = transformJsonOutput(pstate, expr->output, true); + + if (returning->typid != BYTEAOID) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(returning->typid, &typcategory, + &typispreferred); + if (typcategory != TYPCATEGORY_STRING) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), + "JSON_SERIALIZE()"), + errhint("Try returning a string type or bytea."))); + } + } + else + { + /* RETURNING TEXT FORMAT JSON is by default */ + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(JS_FORMAT_JSON, JS_ENC_DEFAULT, -1); + returning->typid = TEXTOID; + returning->typmod = -1; + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), + NULL, returning, false, false, expr->location); +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4cca97ff9c..520d4f2a23 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1953,6 +1953,15 @@ FigureColnameInternal(Node *node, char **name) /* make XMLSERIALIZE act like a regular function */ *name = "xmlserialize"; return 2; + case T_JsonParseExpr: + *name = "json"; + return 2; + case T_JsonScalarExpr: + *name = "json_scalar"; + return 2; + case T_JsonSerializeExpr: + *name = "json_serialize"; + return 2; case T_JsonObjectConstructor: /* make JSON_OBJECT act like a regular function */ *name = "json_object"; diff --git a/src/backend/utils/adt/format_type.c b/src/backend/utils/adt/format_type.c index 12402a0637..36c45a3978 100644 --- a/src/backend/utils/adt/format_type.c +++ b/src/backend/utils/adt/format_type.c @@ -294,6 +294,10 @@ format_type_extended(Oid type_oid, int32 typemod, bits16 flags) else buf = pstrdup("character varying"); break; + + case JSONOID: + buf = pstrdup("json"); + break; } if (buf == NULL) diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index 49080e5fbf..6d8fcdf40d 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -29,21 +29,6 @@ #include "utils/lsyscache.h" #include "utils/typcache.h" -typedef enum /* type categories for datum_to_json */ -{ - JSONTYPE_NULL, /* null, so we didn't bother to identify */ - JSONTYPE_BOOL, /* boolean (built-in types only) */ - JSONTYPE_NUMERIC, /* numeric (ditto) */ - JSONTYPE_DATE, /* we use special formatting for datetimes */ - JSONTYPE_TIMESTAMP, - JSONTYPE_TIMESTAMPTZ, - JSONTYPE_JSON, /* JSON itself (and JSONB) */ - JSONTYPE_ARRAY, /* array */ - JSONTYPE_COMPOSITE, /* composite */ - JSONTYPE_CAST, /* something with an explicit cast to JSON */ - JSONTYPE_OTHER /* all else */ -} JsonTypeCategory; - /* * Support for fast key uniqueness checking. @@ -107,9 +92,6 @@ static void array_dim_to_json(StringInfo result, int dim, int ndims, int *dims, bool use_line_feeds); static void array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds); -static void json_categorize_type(Oid typoid, - JsonTypeCategory *tcategory, - Oid *outfuncoid); static void datum_to_json(Datum val, bool is_null, StringInfo result, JsonTypeCategory tcategory, Oid outfuncoid, bool key_scalar); @@ -188,8 +170,11 @@ json_recv(PG_FUNCTION_ARGS) * Given the datatype OID, return its JsonTypeCategory, as well as the type's * output function OID. If the returned category is JSONTYPE_CAST, we * return the OID of the type->JSON cast function instead. + * + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. */ -static void +void json_categorize_type(Oid typoid, JsonTypeCategory *tcategory, Oid *outfuncoid) @@ -771,6 +756,20 @@ row_to_json_pretty(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); } +/* + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. + */ +Datum +to_json_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid) +{ + StringInfo result = makeStringInfo(); + + datum_to_json(val, false, result, tcategory, outfuncoid, false); + + return PointerGetDatum(cstring_to_text_with_len(result->data, result->len)); +} + /* * Is the given type immutable when coming out of a JSON context? * @@ -821,7 +820,6 @@ to_json(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - StringInfo result; JsonTypeCategory tcategory; Oid outfuncoid; @@ -833,11 +831,7 @@ to_json(PG_FUNCTION_ARGS) json_categorize_type(val_type, &tcategory, &outfuncoid); - result = makeStringInfo(); - - datum_to_json(val, false, result, tcategory, outfuncoid, false); - - PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); + PG_RETURN_DATUM(to_json_worker(val, tcategory, outfuncoid)); } /* diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index cf43c3f2de..7a91177a55 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -34,26 +34,10 @@ typedef struct JsonbInState { JsonbParseState *parseState; JsonbValue *res; + bool unique_keys; Node *escontext; } JsonbInState; -/* unlike with json categories, we need to treat json and jsonb differently */ -typedef enum /* type categories for datum_to_jsonb */ -{ - JSONBTYPE_NULL, /* null, so we didn't bother to identify */ - JSONBTYPE_BOOL, /* boolean (built-in types only) */ - JSONBTYPE_NUMERIC, /* numeric (ditto) */ - JSONBTYPE_DATE, /* we use special formatting for datetimes */ - JSONBTYPE_TIMESTAMP, /* we use special formatting for timestamp */ - JSONBTYPE_TIMESTAMPTZ, /* ... and timestamptz */ - JSONBTYPE_JSON, /* JSON */ - JSONBTYPE_JSONB, /* JSONB */ - JSONBTYPE_ARRAY, /* array */ - JSONBTYPE_COMPOSITE, /* composite */ - JSONBTYPE_JSONCAST, /* something with an explicit cast to JSON */ - JSONBTYPE_OTHER /* all else */ -} JsonbTypeCategory; - typedef struct JsonbAggState { JsonbInState *res; @@ -63,7 +47,8 @@ typedef struct JsonbAggState Oid val_output_func; } JsonbAggState; -static inline Datum jsonb_from_cstring(char *json, int len, Node *escontext); +static inline Datum jsonb_from_cstring(char *json, int len, bool unique_keys, + Node *escontext); static bool checkStringLen(size_t len, Node *escontext); static JsonParseErrorType jsonb_in_object_start(void *pstate); static JsonParseErrorType jsonb_in_object_end(void *pstate); @@ -72,17 +57,11 @@ static JsonParseErrorType jsonb_in_array_end(void *pstate); static JsonParseErrorType jsonb_in_object_field_start(void *pstate, char *fname, bool isnull); static void jsonb_put_escaped_value(StringInfo out, JsonbValue *scalarVal); static JsonParseErrorType jsonb_in_scalar(void *pstate, char *token, JsonTokenType tokentype); -static void jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid); static void composite_to_jsonb(Datum composite, JsonbInState *result); static void array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *dims, Datum *vals, bool *nulls, int *valcount, JsonbTypeCategory tcategory, Oid outfuncoid); static void array_to_jsonb_internal(Datum array, JsonbInState *result); -static void jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid); static void datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, JsonbTypeCategory tcategory, Oid outfuncoid, bool key_scalar); @@ -100,7 +79,7 @@ jsonb_in(PG_FUNCTION_ARGS) { char *json = PG_GETARG_CSTRING(0); - return jsonb_from_cstring(json, strlen(json), fcinfo->context); + return jsonb_from_cstring(json, strlen(json), false, fcinfo->context); } /* @@ -124,7 +103,7 @@ jsonb_recv(PG_FUNCTION_ARGS) else elog(ERROR, "unsupported jsonb version number %d", version); - return jsonb_from_cstring(str, nbytes, NULL); + return jsonb_from_cstring(str, nbytes, false, NULL); } /* @@ -165,6 +144,18 @@ jsonb_send(PG_FUNCTION_ARGS) PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); } +/* + * Note: exported for use in SQL/JSON executor functions. + */ +Datum +jsonb_from_text(text *js, bool unique_keys) +{ + return jsonb_from_cstring(VARDATA_ANY(js), + VARSIZE_ANY_EXHDR(js), + unique_keys, + NULL); +} + /* * Get the type name of a jsonb container. */ @@ -258,7 +249,7 @@ jsonb_typeof(PG_FUNCTION_ARGS) * instead of being thrown. */ static inline Datum -jsonb_from_cstring(char *json, int len, Node *escontext) +jsonb_from_cstring(char *json, int len, bool unique_keys, Node *escontext) { JsonLexContext *lex; JsonbInState state; @@ -268,6 +259,7 @@ jsonb_from_cstring(char *json, int len, Node *escontext) memset(&sem, 0, sizeof(sem)); lex = makeJsonLexContextCstringLen(json, len, GetDatabaseEncoding(), true); + state.unique_keys = unique_keys; state.escontext = escontext; sem.semstate = (void *) &state; @@ -304,6 +296,7 @@ jsonb_in_object_start(void *pstate) JsonbInState *_state = (JsonbInState *) pstate; _state->res = pushJsonbValue(&_state->parseState, WJB_BEGIN_OBJECT, NULL); + _state->parseState->unique_keys = _state->unique_keys; return JSON_SUCCESS; } @@ -639,8 +632,11 @@ add_indent(StringInfo out, bool indent, int level) * Given the datatype OID, return its JsonbTypeCategory, as well as the type's * output function OID. If the returned category is JSONBTYPE_JSONCAST, * we return the OID of the relevant cast function instead. + * + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. */ -static void +void jsonb_categorize_type(Oid typoid, JsonbTypeCategory *tcategory, Oid *outfuncoid) @@ -1150,6 +1146,23 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, datum_to_jsonb(val, is_null, result, tcategory, outfuncoid, key_scalar); } + +/* + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. + */ +Datum +to_jsonb_worker(Datum val, JsonbTypeCategory tcategory, Oid outfuncoid) +{ + JsonbInState result; + + memset(&result, 0, sizeof(JsonbInState)); + + datum_to_jsonb(val, false, &result, tcategory, outfuncoid, false); + + return JsonbPGetDatum(JsonbValueToJsonb(result.res)); +} + /* * Is the given type immutable when coming out of a JSONB context? * @@ -1201,7 +1214,6 @@ to_jsonb(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - JsonbInState result; JsonbTypeCategory tcategory; Oid outfuncoid; @@ -1213,11 +1225,7 @@ to_jsonb(PG_FUNCTION_ARGS) jsonb_categorize_type(val_type, &tcategory, &outfuncoid); - memset(&result, 0, sizeof(JsonbInState)); - - datum_to_jsonb(val, false, &result, tcategory, outfuncoid, false); - - PG_RETURN_POINTER(JsonbValueToJsonb(result.res)); + PG_RETURN_DATUM(to_jsonb_worker(val, tcategory, outfuncoid)); } Datum diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d3a973d86b..d1b03d6cb2 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -10832,6 +10832,15 @@ get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, case JSCTOR_JSON_ARRAY: funcname = "JSON_ARRAY"; break; + case JSCTOR_JSON_PARSE: + funcname = "JSON"; + break; + case JSCTOR_JSON_SCALAR: + funcname = "JSON_SCALAR"; + break; + case JSCTOR_JSON_SERIALIZE: + funcname = "JSON_SERIALIZE"; + break; default: elog(ERROR, "invalid JsonConstructorType %d", ctor->type); } @@ -10879,7 +10888,10 @@ get_json_constructor_options(JsonConstructorExpr *ctor, StringInfo buf) if (ctor->unique) appendStringInfoString(buf, " WITH UNIQUE KEYS"); - get_json_returning(ctor->returning, buf, true); + if (!((ctor->type == JSCTOR_JSON_PARSE || + ctor->type == JSCTOR_JSON_SCALAR) && + ctor->returning->typid == JSONOID)) + get_json_returning(ctor->returning, buf, true); } /* diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 88b03cc472..06a124a86c 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1717,6 +1717,17 @@ typedef struct TriggerTransition /* Nodes for SQL/JSON support */ +/* + * JsonQuotes - + * representation of [KEEP|OMIT] QUOTES clause for JSON_QUERY() + */ +typedef enum JsonQuotes +{ + JS_QUOTES_UNSPEC, /* unspecified */ + JS_QUOTES_KEEP, /* KEEP QUOTES */ + JS_QUOTES_OMIT /* OMIT QUOTES */ +} JsonQuotes; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1740,6 +1751,43 @@ typedef struct JsonKeyValue JsonValueExpr *value; /* JSON value expression */ } JsonKeyValue; +/* + * JsonParseExpr - + * untransformed representation of JSON() + */ +typedef struct JsonParseExpr +{ + NodeTag type; + JsonValueExpr *expr; /* string expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + bool unique_keys; /* WITH UNIQUE KEYS? */ + int location; /* token location, or -1 if unknown */ +} JsonParseExpr; + +/* + * JsonScalarExpr - + * untransformed representation of JSON_SCALAR() + */ +typedef struct JsonScalarExpr +{ + NodeTag type; + Expr *expr; /* scalar expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonScalarExpr; + +/* + * JsonSerializeExpr - + * untransformed representation of JSON_SERIALIZE() function + */ +typedef struct JsonSerializeExpr +{ + NodeTag type; + JsonValueExpr *expr; /* json value expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonSerializeExpr; + /* * JsonObjectConstructor - * untransformed representation of JSON_OBJECT() constructor diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 792a743f72..6c6a1810af 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1607,7 +1607,10 @@ typedef enum JsonConstructorType JSCTOR_JSON_OBJECT = 1, JSCTOR_JSON_ARRAY = 2, JSCTOR_JSON_OBJECTAGG = 3, - JSCTOR_JSON_ARRAYAGG = 4 + JSCTOR_JSON_ARRAYAGG = 4, + JSCTOR_JSON_SCALAR = 5, + JSCTOR_JSON_SERIALIZE = 6, + JSCTOR_JSON_PARSE = 7 } JsonConstructorType; /* diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index f5b2e61ca5..5984dcfa4b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -230,11 +230,13 @@ PG_KEYWORD("is", IS, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("isnull", ISNULL, TYPE_FUNC_NAME_KEYWORD, AS_LABEL) PG_KEYWORD("isolation", ISOLATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) -PG_KEYWORD("json", JSON, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/json.h b/src/include/utils/json.h index 35a9a5545d..4c0e0bd09d 100644 --- a/src/include/utils/json.h +++ b/src/include/utils/json.h @@ -16,11 +16,30 @@ #include "lib/stringinfo.h" +typedef enum /* type categories for datum_to_json */ +{ + JSONTYPE_NULL, /* null, so we didn't bother to identify */ + JSONTYPE_BOOL, /* boolean (built-in types only) */ + JSONTYPE_NUMERIC, /* numeric (ditto) */ + JSONTYPE_DATE, /* we use special formatting for datetimes */ + JSONTYPE_TIMESTAMP, + JSONTYPE_TIMESTAMPTZ, + JSONTYPE_JSON, /* JSON itself (and JSONB) */ + JSONTYPE_ARRAY, /* array */ + JSONTYPE_COMPOSITE, /* composite */ + JSONTYPE_CAST, /* something with an explicit cast to JSON */ + JSONTYPE_OTHER /* all else */ +} JsonTypeCategory; + /* functions in json.c */ extern void escape_json(StringInfo buf, const char *str); extern char *JsonEncodeDateTime(char *buf, Datum value, Oid typid, const int *tzp); extern bool to_json_is_immutable(Oid typoid); +extern void json_categorize_type(Oid typoid, JsonTypeCategory *tcategory, + Oid *outfuncoid); +extern Datum to_json_worker(Datum val, JsonTypeCategory tcategory, + Oid outfuncoid); extern Datum json_build_object_worker(int nargs, Datum *args, bool *nulls, Oid *types, bool absent_on_null, bool unique_keys); diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index 649a1644f2..8417a74298 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -368,6 +368,22 @@ typedef struct JsonbIterator struct JsonbIterator *parent; } JsonbIterator; +/* unlike with json categories, we need to treat json and jsonb differently */ +typedef enum /* type categories for datum_to_jsonb */ +{ + JSONBTYPE_NULL, /* null, so we didn't bother to identify */ + JSONBTYPE_BOOL, /* boolean (built-in types only) */ + JSONBTYPE_NUMERIC, /* numeric (ditto) */ + JSONBTYPE_DATE, /* we use special formatting for datetimes */ + JSONBTYPE_TIMESTAMP, /* we use special formatting for timestamp */ + JSONBTYPE_TIMESTAMPTZ, /* ... and timestamptz */ + JSONBTYPE_JSON, /* JSON */ + JSONBTYPE_JSONB, /* JSONB */ + JSONBTYPE_ARRAY, /* array */ + JSONBTYPE_COMPOSITE, /* composite */ + JSONBTYPE_JSONCAST, /* something with an explicit cast to JSON */ + JSONBTYPE_OTHER /* all else */ +} JsonbTypeCategory; /* Convenience macros */ static inline Jsonb * @@ -418,6 +434,7 @@ extern void JsonbHashScalarValueExtended(const JsonbValue *scalarVal, uint64 *hash, uint64 seed); /* jsonb.c support functions */ +extern Datum jsonb_from_text(text *js, bool unique_keys); extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, @@ -430,6 +447,10 @@ extern Datum jsonb_set_element(Jsonb *jb, Datum *path, int path_len, extern Datum jsonb_get_element(Jsonb *jb, Datum *path, int npath, bool *isnull, bool as_text); extern bool to_jsonb_is_immutable(Oid typoid); +extern void jsonb_categorize_type(Oid typoid, JsonbTypeCategory *tcategory, + Oid *outfuncoid); +extern Datum to_jsonb_worker(Datum val, JsonbTypeCategory tcategory, + Oid outfuncoid); extern Datum jsonb_build_object_worker(int nargs, Datum *args, bool *nulls, Oid *types, bool absent_on_null, bool unique_keys); diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index d73c7e2c6c..f0f5ae8119 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -1,3 +1,337 @@ +-- JSON() +SELECT JSON(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON(); + ^ +SELECT JSON(NULL); + json +------ + +(1 row) + +SELECT JSON('{ "a" : 1 } '); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +ERROR: JSON ENCODING clause is only allowed for bytea input type +LINE 1: SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); + ^ +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + pg_typeof +----------- + json +(1 row) + +SELECT JSON(' 1 '::json); + json +--------- + 1 +(1 row) + +SELECT JSON(' 1 '::jsonb); + json +------ + 1 +(1 row) + +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +ERROR: cannot use non-string types with WITH UNIQUE KEYS clause +LINE 1: SELECT JSON(' 1 '::json WITH UNIQUE KEYS); + ^ +SELECT JSON(123); +ERROR: cannot cast type integer to json +LINE 1: SELECT JSON(123); + ^ +SELECT JSON('{"a": 1, "a": 2}'); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +ERROR: duplicate JSON object key value +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); + QUERY PLAN +----------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); + QUERY PLAN +------------------------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON ENCODING UTF8) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); + QUERY PLAN +---------------------------------------------- + Result + Output: JSON('123'::text WITH UNIQUE KEYS) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +SELECT JSON('123' RETURNING text); +ERROR: cannot use RETURNING type text in JSON() +LINE 1: SELECT JSON('123' RETURNING text); + ^ +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING json); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING jsonb); + QUERY PLAN +---------------------------------------------- + Result + Output: JSON('123'::jsonb RETURNING jsonb) +(2 rows) + +SELECT pg_typeof(JSON('123')); + pg_typeof +----------- + json +(1 row) + +SELECT pg_typeof(JSON('123' RETURNING json)); + pg_typeof +----------- + json +(1 row) + +SELECT pg_typeof(JSON('123' RETURNING jsonb)); + pg_typeof +----------- + jsonb +(1 row) + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SCALAR(); + ^ +SELECT JSON_SCALAR(NULL); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(NULL::int); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(123); + json_scalar +------------- + 123 +(1 row) + +SELECT JSON_SCALAR(123.45); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(123.45::numeric); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(true); + json_scalar +------------- + true +(1 row) + +SELECT JSON_SCALAR(false); + json_scalar +------------- + false +(1 row) + +SELECT JSON_SCALAR(' 123.45'); + json_scalar +------------- + " 123.45" +(1 row) + +SELECT JSON_SCALAR('2020-06-07'::date); + json_scalar +-------------- + "2020-06-07" +(1 row) + +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); + json_scalar +----------------------- + "2020-06-07T01:02:03" +(1 row) + +SELECT JSON_SCALAR('{}'::json); + json_scalar +------------- + {} +(1 row) + +SELECT JSON_SCALAR('{}'::jsonb); + json_scalar +------------- + {} +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); + QUERY PLAN +---------------------------- + Result + Output: JSON_SCALAR(123) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); + QUERY PLAN +------------------------------------ + Result + Output: JSON_SCALAR('123'::text) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING json); + QUERY PLAN +---------------------------- + Result + Output: JSON_SCALAR(123) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING jsonb); + QUERY PLAN +-------------------------------------------- + Result + Output: JSON_SCALAR(123 RETURNING jsonb) +(2 rows) + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SERIALIZE(); + ^ +SELECT JSON_SERIALIZE(NULL); + json_serialize +---------------- + +(1 row) + +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } '); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('1'); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('1' FORMAT JSON); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); + json_serialize +---------------------------- + \x7b20226122203a2031207d20 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + pg_typeof +----------- + text +(1 row) + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); +ERROR: cannot use RETURNING type jsonb in JSON_SERIALIZE() +HINT: Try returning a string type or bytea. +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); + QUERY PLAN +----------------------------------------------------- + Result + Output: JSON_SERIALIZE('{}'::json RETURNING text) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + QUERY PLAN +------------------------------------------------------ + Result + Output: JSON_SERIALIZE('{}'::json RETURNING bytea) +(2 rows) + -- JSON_OBJECT() SELECT JSON_OBJECT(); json_object @@ -630,6 +964,13 @@ ERROR: duplicate JSON object key SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); ERROR: duplicate JSON object key +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + json_objectagg +------------------ + {"1": 1, "2": 2} +(1 row) + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -645,6 +986,41 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); CREATE OR REPLACE VIEW public.json_object_view AS SELECT JSON_OBJECT('foo' : '1'::text FORMAT JSON, 'bar' : 'baz'::text RETURNING json) AS "json_object" DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + a | json_objectagg +---------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(2 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":1,"v":null} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1, "2" : 2 } + {"k":1,"v":null} | { "1" : 1, "2" : 2 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 4fd820fd51..e338ef706b 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -1,3 +1,75 @@ +-- JSON() +SELECT JSON(); +SELECT JSON(NULL); +SELECT JSON('{ "a" : 1 } '); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + +SELECT JSON(' 1 '::json); +SELECT JSON(' 1 '::jsonb); +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +SELECT JSON(123); + +SELECT JSON('{"a": 1, "a": 2}'); +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + +SELECT JSON('123' RETURNING text); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING json); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING jsonb); +SELECT pg_typeof(JSON('123')); +SELECT pg_typeof(JSON('123' RETURNING json)); +SELECT pg_typeof(JSON('123' RETURNING jsonb)); + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +SELECT JSON_SCALAR(NULL); +SELECT JSON_SCALAR(NULL::int); +SELECT JSON_SCALAR(123); +SELECT JSON_SCALAR(123.45); +SELECT JSON_SCALAR(123.45::numeric); +SELECT JSON_SCALAR(true); +SELECT JSON_SCALAR(false); +SELECT JSON_SCALAR(' 123.45'); +SELECT JSON_SCALAR('2020-06-07'::date); +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); +SELECT JSON_SCALAR('{}'::json); +SELECT JSON_SCALAR('{}'::jsonb); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING json); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING jsonb); + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +SELECT JSON_SERIALIZE(NULL); +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); +SELECT JSON_SERIALIZE('{ "a" : 1 } '); +SELECT JSON_SERIALIZE('1'); +SELECT JSON_SERIALIZE('1' FORMAT JSON); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); + + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + -- JSON_OBJECT() SELECT JSON_OBJECT(); SELECT JSON_OBJECT(RETURNING json); @@ -216,6 +288,9 @@ FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -227,6 +302,24 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e941fb6c82..7d60511e9e 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1296,6 +1296,7 @@ JsonPathPredicateCallback JsonPathString JsonReturning JsonSemAction +JsonSerializeExpr JsonTokenType JsonTransformStringValuesAction JsonTypeCategory -- 2.35.3
v2-0005-JSON_TABLE.patch (application/octet-stream, 160.3K) download From 2725e450971ee951b05a815115576a8d9881a957 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:14:12 +0900 Subject: [PATCH v2 5/6] JSON_TABLE This feature allows jsonb data to be treated as a table and thus used in a FROM clause like other tabular data. Data can be selected from the jsonb using jsonpath expressions, and hoisted out of nested structures in the jsonb to form multiple rows, more or less like an outer join. This also adds the PLAN clauses for JSON_TABLE, which allow the user to specify how data from nested paths are joined, allowing considerable freedom in shaping the tabular output of JSON_TABLE. PLAN DEFAULT allows the user to specify the global strategies when dealing with sibling or child nested paths. The is often sufficient to achieve the necessary goal, and is considerably simpler than the full PLAN clause, which allows the user to specify the strategy to be used for each named nested path. Nikita Glukhov Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zhihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby. Discussion: https://postgr.es/m/7e2cb85d-24cf-4abb-30a5-1a33715959bd@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org --- doc/src/sgml/func.sgml | 448 ++++++++ src/backend/commands/explain.c | 8 +- src/backend/executor/execExprInterp.c | 5 + src/backend/executor/nodeTableFuncscan.c | 27 +- src/backend/nodes/makefuncs.c | 19 + src/backend/nodes/nodeFuncs.c | 30 + src/backend/parser/Makefile | 1 + src/backend/parser/gram.y | 477 ++++++++- src/backend/parser/meson.build | 1 + src/backend/parser/parse_clause.c | 14 +- src/backend/parser/parse_expr.c | 35 +- src/backend/parser/parse_jsontable.c | 739 +++++++++++++ src/backend/parser/parse_relation.c | 5 +- src/backend/parser/parse_target.c | 3 + src/backend/utils/adt/jsonpath_exec.c | 543 ++++++++++ src/backend/utils/adt/ruleutils.c | 279 ++++- src/include/nodes/execnodes.h | 2 + src/include/nodes/makefuncs.h | 2 + src/include/nodes/parsenodes.h | 90 ++ src/include/nodes/primnodes.h | 61 +- src/include/parser/kwlist.h | 4 + src/include/parser/parse_clause.h | 3 + src/include/utils/jsonpath.h | 3 + src/test/regress/expected/json_sqljson.out | 6 + src/test/regress/expected/jsonb_sqljson.out | 1069 +++++++++++++++++++ src/test/regress/sql/json_sqljson.sql | 4 + src/test/regress/sql/jsonb_sqljson.sql | 629 +++++++++++ src/tools/pgindent/typedefs.list | 13 + 28 files changed, 4487 insertions(+), 33 deletions(-) create mode 100644 src/backend/parser/parse_jsontable.c diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index e7a018583a..74b017d2da 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -17144,6 +17144,454 @@ array w/o UK? | t </sect2> + <sect2 id="functions-sqljson-table"> + <title>JSON_TABLE</title> + <indexterm> + <primary>json_table</primary> + </indexterm> + + <para> + <function>json_table</function> is an SQL/JSON function which + queries <acronym>JSON</acronym> data + and presents the results as a relational view, which can be accessed as a + regular SQL table. You can only use <function>json_table</function> inside the + <literal>FROM</literal> clause of a <literal>SELECT</literal> statement. + </para> + + <para> + Taking JSON data as input, <function>json_table</function> uses + a path expression to extract a part of the provided data that + will be used as a <firstterm>row pattern</firstterm> for the + constructed view. Each SQL/JSON item at the top level of the row pattern serves + as the source for a separate row in the constructed relational view. + </para> + + <para> + To split the row pattern into columns, <function>json_table</function> + provides the <literal>COLUMNS</literal> clause that defines the + schema of the created view. For each column to be constructed, + this clause provides a separate path expression that evaluates + the row pattern, extracts a JSON item, and returns it as a + separate SQL value for the specified column. If the required value + is stored in a nested level of the row pattern, it can be extracted + using the <literal>NESTED PATH</literal> subclause. Joining the + columns returned by <literal>NESTED PATH</literal> can add multiple + new rows to the constructed view. Such rows are called + <firstterm>child rows</firstterm>, as opposed to the <firstterm>parent row</firstterm> + that generates them. + </para> + + <para> + The rows produced by <function>JSON_TABLE</function> are laterally + joined to the row that generated them, so you do not have to explicitly join + the constructed view with the original table holding <acronym>JSON</acronym> + data. Optionally, you can specify how to join the columns returned + by <literal>NESTED PATH</literal> using the <literal>PLAN</literal> clause. + </para> + + <para> + Each <literal>NESTED PATH</literal> clause can generate one or more + columns. Columns produced by <literal>NESTED PATH</literal>s at the + same level are considered to be <firstterm>siblings</firstterm>, + while a column produced by a <literal>NESTED PATH</literal> is + considered to be a child of the column produced by a + <literal>NESTED PATH</literal> or row expression at a higher level. + Sibling columns are always joined first. Once they are processed, + the resulting rows are joined to the parent row. + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional></literal> + </term> + <listitem> + <para> + The input data to query, the JSON path expression defining the query, + and an optional <literal>PASSING</literal> clause, which can provide data + values to the <replaceable>path_expression</replaceable>. + The result of the input data + evaluation is called the <firstterm>row pattern</firstterm>. The row + pattern is used as the source for row values in the constructed view. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>COLUMNS</literal>( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + The <literal>COLUMNS</literal> clause defining the schema of the + constructed view. In this clause, you must specify all the columns + to be filled with SQL/JSON items. + The <replaceable>json_table_column</replaceable> + expression has the following syntax variants: + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>name</replaceable> <replaceable>type</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional></literal> + </term> + <listitem> + + <para> + Inserts a single SQL/JSON item into each row of + the specified column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable> + and fills the column with produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>ON EMPTY</literal> and + <literal>ON ERROR</literal> clauses to define how to handle missing values + or structural errors. + <literal>WRAPPER</literal> and <literal>QUOTES</literal> clauses can only + be used with JSON, array, and composite types. + These clauses have the same syntax and semantics as for + <function>json_value</function> and <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> <literal>FORMAT</literal> <replaceable>json_representation</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a composite SQL/JSON + item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable> + and fills the column with produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>WRAPPER</literal>, <literal>QUOTES</literal>, + <literal>ON EMPTY</literal> and <literal>ON ERROR</literal> clauses + to define additional settings for the returned SQL/JSON items. + These clauses have the same syntax and semantics as + for <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> + <literal>EXISTS</literal> <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a boolean item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable>, + checks whether any SQL/JSON items were returned, and fills the column with + resulting boolean value, one for each row. + The specified <replaceable>type</replaceable> should have cast from + <type>boolean</type>. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + </para> + <para> + Optionally, you can add <literal>ON ERROR</literal> clause to define + error behavior. This clause has the same syntax and semantics as + for <function>json_exists</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>NESTED PATH</literal> <replaceable>json_path_specification</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> + <literal>COLUMNS</literal> ( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + Extracts SQL/JSON items from nested levels of the row pattern, + generates one or more columns as defined by the <literal>COLUMNS</literal> + subclause, and inserts the extracted SQL/JSON items into each row of these columns. + The <replaceable>json_table_column</replaceable> expression in the + <literal>COLUMNS</literal> subclause uses the same syntax as in the + parent <literal>COLUMNS</literal> clause. + </para> + + <para> + The <literal>NESTED PATH</literal> syntax is recursive, + so you can go down multiple nested levels by specifying several + <literal>NESTED PATH</literal> subclauses within each other. + It allows to unnest the hierarchy of JSON objects and arrays + in a single function invocation rather than chaining several + <function>JSON_TABLE</function> expressions in an SQL statement. + </para> + + <para> + You can use the <literal>PLAN</literal> clause to define how + to join the columns returned by <literal>NESTED PATH</literal> clauses. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <literal>FOR ORDINALITY</literal> + </term> + <listitem> + + <para> + Adds an ordinality column that provides sequential row numbering. + You can have only one ordinality column per table. Row numbering + is 1-based. For child rows that result from the <literal>NESTED PATH</literal> + clauses, the parent row number is repeated. + </para> + </listitem> + </varlistentry> + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>AS</literal> <replaceable>json_path_name</replaceable> + </term> + <listitem> + + <para> + The optional <replaceable>json_path_name</replaceable> serves as an + identifier of the provided <replaceable>json_path_specification</replaceable>. + The path name must be unique and distinct from the column names. + When using the <literal>PLAN</literal> clause, you must specify the names + for all the paths, including the row pattern. Each path name can appear in + the <literal>PLAN</literal> clause only once. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN</literal> ( <replaceable>json_table_plan</replaceable> ) + </term> + <listitem> + + <para> + Defines how to join the data returned by <literal>NESTED PATH</literal> + clauses to the constructed view. + </para> + <para> + To join columns with parent/child relationship, you can use: + </para> + <variablelist> + <varlistentry> + <term> + <literal>INNER</literal> + </term> + <listitem> + + <para> + Use <literal>INNER JOIN</literal>, so that the parent row + is omitted from the output if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>OUTER</literal> + </term> + <listitem> + + <para> + Use <literal>LEFT OUTER JOIN</literal>, so that the parent row + is always included into the output even if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>, with NULL values + inserted into the child columns if the corresponding + values are missing. + </para> + <para> + This is the default option for joining columns with parent/child relationship. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para> + To join sibling columns, you can use: + </para> + + <variablelist> + <varlistentry> + <term> + <literal>UNION</literal> + </term> + <listitem> + + <para> + Generate one row for each value produced by each of the sibling + columns. The columns from the other siblings are set to null. + </para> + <para> + This is the default option for joining sibling columns. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>CROSS</literal> + </term> + <listitem> + + <para> + Generate one row for each combination of values from the sibling columns. + </para> + </listitem> + </varlistentry> + + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN DEFAULT</literal> ( <literal><replaceable>OUTER | INNER</replaceable> <optional>, <replaceable>UNION | CROSS</replaceable> </optional></literal> ) + </term> + <listitem> + <para> + The terms can also be specified in reverse order. The + <literal>INNER</literal> or <literal>OUTER</literal> option defines the + joining plan for parent/child columns, while <literal>UNION</literal> or + <literal>CROSS</literal> affects joins of sibling columns. This form + of <literal>PLAN</literal> overrides the default plan for + all columns at once. Even though the path names are not included in the + <literal>PLAN DEFAULT</literal> form, to conform to the SQL/JSON standard + they must be provided for all the paths if the <literal>PLAN</literal> + clause is used. + </para> + <para> + <literal>PLAN DEFAULT</literal> is simpler than specifying a complete + <literal>PLAN</literal>, and is often all that is required to get the desired + output. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para>Examples</para> + + <para> + In these examples the following small table storing some JSON data will be used: +<programlisting> +CREATE TABLE my_films ( js jsonb ); + +INSERT INTO my_films VALUES ( +'{ "favorites" : [ + { "kind" : "comedy", "films" : [ + { "title" : "Bananas", + "director" : "Woody Allen"}, + { "title" : "The Dinner Game", + "director" : "Francis Veber" } ] }, + { "kind" : "horror", "films" : [ + { "title" : "Psycho", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "thriller", "films" : [ + { "title" : "Vertigo", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "drama", "films" : [ + { "title" : "Yojimbo", + "director" : "Akira Kurosawa" } ] } + ] }'); +</programlisting> + </para> + <para> + Query the <structname>my_films</structname> table holding + some JSON data about the films and create a view that + distributes the film genre, title, and director between separate columns: +<screen> +SELECT jt.* FROM + my_films, + JSON_TABLE ( js, '$.favorites[*]' COLUMNS ( + id FOR ORDINALITY, + kind text PATH '$.kind', + NESTED PATH '$.films[*]' COLUMNS ( + title text PATH '$.title', + director text PATH '$.director'))) AS jt; +----+----------+------------------+------------------- + id | kind | title | director +----+----------+------------------+------------------- + 1 | comedy | Bananas | Woody Allen + 1 | comedy | The Dinner Game | Francis Veber + 2 | horror | Psycho | Alfred Hitchcock + 3 | thriller | Vertigo | Alfred Hitchcock + 4 | drama | Yojimbo | Akira Kurosawa + (5 rows) +</screen> + </para> + + <para> + Find a director that has done films in two different genres: +<screen> +SELECT + director1 AS director, title1, kind1, title2, kind2 +FROM + my_films, + JSON_TABLE ( js, '$.favorites' AS favs COLUMNS ( + NESTED PATH '$[*]' AS films1 COLUMNS ( + kind1 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film1 COLUMNS ( + title1 text PATH '$.title', + director1 text PATH '$.director') + ), + NESTED PATH '$[*]' AS films2 COLUMNS ( + kind2 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film2 COLUMNS ( + title2 text PATH '$.title', + director2 text PATH '$.director' + ) + ) + ) + PLAN (favs OUTER ((films1 INNER film1) CROSS (films2 INNER film2))) + ) AS jt + WHERE kind1 > kind2 AND director1 = director2; + + director | title1 | kind1 | title2 | kind2 +------------------+---------+----------+--------+-------- + Alfred Hitchcock | Vertigo | thriller | Psycho | horror +(1 row) +</screen> + </para> + </sect2> + <sect2 id="functions-sqljson-path"> <title>The SQL/JSON Path Language</title> diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 8570b14f62..ad899de5d6 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -3862,7 +3862,13 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es) break; case T_TableFuncScan: Assert(rte->rtekind == RTE_TABLEFUNC); - objectname = "xmltable"; + if (rte->tablefunc) + if (rte->tablefunc->functype == TFT_XMLTABLE) + objectname = "xmltable"; + else /* Must be TFT_JSON_TABLE */ + objectname = "json_table"; + else + objectname = NULL; objecttag = "Table Function Name"; break; case T_ValuesScan: diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 4b0647153a..584b993d0a 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -4308,6 +4308,11 @@ ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) break; } + case JSON_TABLE_OP: + res = item; + resnull = false; + break; + default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); *op->resnull = true; diff --git a/src/backend/executor/nodeTableFuncscan.c b/src/backend/executor/nodeTableFuncscan.c index 791cbd2372..085a612533 100644 --- a/src/backend/executor/nodeTableFuncscan.c +++ b/src/backend/executor/nodeTableFuncscan.c @@ -28,6 +28,7 @@ #include "miscadmin.h" #include "nodes/execnodes.h" #include "utils/builtins.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/xml.h" @@ -161,8 +162,9 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) scanstate->ss.ps.qual = ExecInitQual(node->scan.plan.qual, &scanstate->ss.ps); - /* Only XMLTABLE is supported currently */ - scanstate->routine = &XmlTableRoutine; + /* Only XMLTABLE and JSON_TABLE are supported currently */ + scanstate->routine = + tf->functype == TFT_XMLTABLE ? &XmlTableRoutine : &JsonbTableRoutine; scanstate->perTableCxt = AllocSetContextCreate(CurrentMemoryContext, @@ -182,6 +184,10 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) ExecInitExprList(tf->colexprs, (PlanState *) scanstate); scanstate->coldefexprs = ExecInitExprList(tf->coldefexprs, (PlanState *) scanstate); + scanstate->colvalexprs = + ExecInitExprList(tf->colvalexprs, (PlanState *) scanstate); + scanstate->passingvalexprs = + ExecInitExprList(tf->passingvalexprs, (PlanState *) scanstate); scanstate->notnulls = tf->notnulls; @@ -381,14 +387,17 @@ tfuncInitialize(TableFuncScanState *tstate, ExprContext *econtext, Datum doc) routine->SetNamespace(tstate, ns_name, ns_uri); } - /* Install the row filter expression into the table builder context */ - value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); - if (isnull) - ereport(ERROR, - (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), - errmsg("row filter expression must not be null"))); + if (routine->SetRowFilter) + { + /* Install the row filter expression into the table builder context */ + value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); + if (isnull) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("row filter expression must not be null"))); - routine->SetRowFilter(tstate, TextDatumGetCString(value)); + routine->SetRowFilter(tstate, TextDatumGetCString(value)); + } /* * Install the column filter expressions into the table builder context. diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 5dc3aa2e9f..84eb33b1db 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -874,6 +874,25 @@ makeJsonBehavior(JsonBehaviorType type, Node *default_expr) return behavior; } +/* + * makeJsonTableJoinedPlan - + * creates a joined JsonTablePlan node + */ +Node * +makeJsonTableJoinedPlan(JsonTablePlanJoinType type, Node *plan1, Node *plan2, + int location) +{ + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_JOINED; + n->join_type = type; + n->plan1 = castNode(JsonTablePlan, plan1); + n->plan2 = castNode(JsonTablePlan, plan2); + n->location = location; + + return (Node *) n; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index d31371bb4d..e07c066e15 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -2631,6 +2631,10 @@ expression_tree_walker_impl(Node *node, return true; if (WALK(tf->coldefexprs)) return true; + if (WALK(tf->colvalexprs)) + return true; + if (WALK(tf->passingvalexprs)) + return true; } break; default: @@ -3699,6 +3703,8 @@ expression_tree_mutator_impl(Node *node, MUTATE(newnode->rowexpr, tf->rowexpr, Node *); MUTATE(newnode->colexprs, tf->colexprs, List *); MUTATE(newnode->coldefexprs, tf->coldefexprs, List *); + MUTATE(newnode->colvalexprs, tf->colvalexprs, List *); + MUTATE(newnode->passingvalexprs, tf->passingvalexprs, List *); return (Node *) newnode; } break; @@ -4130,6 +4136,30 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonTable: + { + JsonTable *jt = (JsonTable *) node; + + if (WALK(jt->common)) + return true; + if (WALK(jt->columns)) + return true; + } + break; + case T_JsonTableColumn: + { + JsonTableColumn *jtc = (JsonTableColumn *) node; + + if (WALK(jtc->typeName)) + return true; + if (WALK(jtc->on_empty)) + return true; + if (WALK(jtc->on_error)) + return true; + if (jtc->coltype == JTC_NESTED && WALK(jtc->columns)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile index 9f1c4022bb..f4c0cc7f10 100644 --- a/src/backend/parser/Makefile +++ b/src/backend/parser/Makefile @@ -23,6 +23,7 @@ OBJS = \ parse_enr.o \ parse_expr.o \ parse_func.o \ + parse_jsontable.o \ parse_merge.o \ parse_node.o \ parse_oper.o \ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 4f5c44e063..91de9c1ea5 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -656,19 +656,44 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_aggregate_func json_api_common_syntax json_argument + json_table + json_table_column_definition + json_table_ordinality_column_definition + json_table_regular_column_definition + json_table_formatted_column_definition + json_table_exists_column_definition + json_table_nested_columns + json_table_plan_clause_opt + json_table_plan + json_table_plan_simple + json_table_plan_parent_child + json_table_plan_outer + json_table_plan_inner + json_table_plan_sibling + json_table_plan_union + json_table_plan_cross + json_table_plan_primary %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt json_arguments + json_table_columns_clause + json_table_column_definition_list + +%type <str> json_table_column_path_specification_clause_opt %type <ival> json_encoding_clause_opt json_predicate_type_constraint + json_table_default_plan_choices + json_table_default_plan_inner_outer + json_table_default_plan_union_cross json_wrapper_behavior %type <jsbehavior> json_value_behavior json_query_behavior json_exists_behavior + json_table_behavior %type <js_quotes> json_quotes_clause_opt @@ -739,7 +764,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG - JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_TABLE JSON_VALUE KEY KEYS KEEP @@ -750,8 +775,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE - NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE - NORMALIZE NORMALIZED + NAME_P NAMES NATIONAL NATURAL NCHAR NESTED NEW NEXT NFC NFD NFKC NFKD NO + NONE NORMALIZE NORMALIZED NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC @@ -759,8 +784,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER - PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD - PLACING PLANS POLICY + PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH + PLACING PLAN PLANS POLICY POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION @@ -868,6 +893,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); * Using the same precedence as IDENT seems right for the reasons given above. */ %nonassoc UNBOUNDED /* ideally would have same precedence as IDENT */ +%nonassoc COLUMNS %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' @@ -890,6 +916,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL +%nonassoc json_table_column +%nonassoc NESTED +%left PATH %% /* @@ -13331,6 +13360,21 @@ table_ref: relation_expr opt_alias_clause $2->alias = $4; $$ = (Node *) $2; } + | json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $1); + + jt->alias = $2; + $$ = (Node *) jt; + } + | LATERAL_P json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $2); + + jt->alias = $3; + jt->lateral = true; + $$ = (Node *) jt; + } ; @@ -13898,6 +13942,8 @@ xmltable_column_option_el: { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(true), @1); } | NULL_P { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(false), @1); } + | PATH b_expr + { $$ = makeDefElem("path", $2, @1); } ; xml_namespace_list: @@ -16704,6 +16750,11 @@ json_value_behavior: | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } ; +json_table_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + ; + /* ARRAY is a noise word */ json_wrapper_behavior: WITHOUT WRAPPER { $$ = JSW_NONE; } @@ -16725,6 +16776,414 @@ json_quotes_clause_opt: | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } ; +json_table: + JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + json_table_behavior ON ERROR_P + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_columns_clause: + COLUMNS '(' json_table_column_definition_list ')' { $$ = $3; } + ; + +json_table_column_definition_list: + json_table_column_definition + { $$ = list_make1($1); } + | json_table_column_definition_list ',' json_table_column_definition + { $$ = lappend($1, $3); } + ; + +json_table_column_definition: + json_table_ordinality_column_definition %prec json_table_column + | json_table_regular_column_definition %prec json_table_column + | json_table_formatted_column_definition %prec json_table_column + | json_table_exists_column_definition %prec json_table_column + | json_table_nested_columns + ; + +json_table_ordinality_column_definition: + ColId FOR ORDINALITY + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FOR_ORDINALITY; + n->name = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_regular_column_definition: + ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_exists_column_definition: + ColId Typename + EXISTS json_table_column_path_specification_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + EXISTS json_table_column_path_specification_clause_opt + json_exists_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_column_path_specification_clause_opt: + PATH Sconst { $$ = $2; } + | /* EMPTY */ %prec json_table_column { $$ = NULL; } + ; + +json_table_formatted_column_definition: + ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->on_error = $12; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_nested_columns: + NESTED path_opt Sconst + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = NULL; + n->columns = $4; + n->location = @1; + $$ = (Node *) n; + } + | NESTED path_opt Sconst AS name + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = $5; + n->columns = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +path_opt: + PATH { } + | /* EMPTY */ { } + ; + +json_table_plan_clause_opt: + PLAN '(' json_table_plan ')' { $$ = $3; } + | PLAN DEFAULT '(' json_table_default_plan_choices ')' + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_DEFAULT; + n->join_type = $4; + n->location = @1; + $$ = (Node *) n; + } + | /* EMPTY */ { $$ = NULL; } + ; + +json_table_plan: + json_table_plan_simple + | json_table_plan_parent_child + | json_table_plan_sibling + ; + +json_table_plan_simple: + name + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_SIMPLE; + n->pathname = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_plan_primary: + json_table_plan_simple { $$ = $1; } + | '(' json_table_plan ')' + { + castNode(JsonTablePlan, $2)->location = @1; + $$ = $2; + } + ; + +json_table_plan_outer: + json_table_plan_simple OUTER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_OUTER, $1, $3, @1); } + ; + +json_table_plan_inner: + json_table_plan_simple INNER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_INNER, $1, $3, @1); } + ; + +json_table_plan_parent_child: + json_table_plan_outer + | json_table_plan_inner + ; + +json_table_plan_union: + json_table_plan_primary UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + | json_table_plan_union UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + ; + +json_table_plan_cross: + json_table_plan_primary CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + | json_table_plan_cross CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + ; + +json_table_plan_sibling: + json_table_plan_union + | json_table_plan_cross + ; + +json_table_default_plan_choices: + json_table_default_plan_inner_outer { $$ = $1 | JSTPJ_UNION; } + | json_table_default_plan_union_cross { $$ = $1 | JSTPJ_OUTER; } + | json_table_default_plan_inner_outer ',' + json_table_default_plan_union_cross { $$ = $1 | $3; } + | json_table_default_plan_union_cross ',' + json_table_default_plan_inner_outer { $$ = $1 | $3; } + ; + +json_table_default_plan_inner_outer: + INNER_P { $$ = JSTPJ_INNER; } + | OUTER_P { $$ = JSTPJ_OUTER; } + ; + +json_table_default_plan_union_cross: + UNION { $$ = JSTPJ_UNION; } + | CROSS { $$ = JSTPJ_CROSS; } + ; + json_returning_clause_opt: RETURNING Typename { @@ -17463,6 +17922,7 @@ unreserved_keyword: | MOVE | NAME_P | NAMES + | NESTED | NEW | NEXT | NFC @@ -17497,6 +17957,8 @@ unreserved_keyword: | PARTITION | PASSING | PASSWORD + | PATH + | PLAN | PLANS | POLICY | PRECEDING @@ -17661,6 +18123,7 @@ col_name_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | LEAST | NATIONAL @@ -18029,6 +18492,7 @@ bare_label_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | KEEP | KEY @@ -18068,6 +18532,7 @@ bare_label_keyword: | NATIONAL | NATURAL | NCHAR + | NESTED | NEW | NEXT | NFC @@ -18112,7 +18577,9 @@ bare_label_keyword: | PARTITION | PASSING | PASSWORD + | PATH | PLACING + | PLAN | PLANS | POLICY | POSITION diff --git a/src/backend/parser/meson.build b/src/backend/parser/meson.build index 94517a7f7d..0f0e4ac7ca 100644 --- a/src/backend/parser/meson.build +++ b/src/backend/parser/meson.build @@ -10,6 +10,7 @@ backend_sources += files( 'parse_enr.c', 'parse_expr.c', 'parse_func.c', + 'parse_jsontable.c', 'parse_merge.c', 'parse_node.c', 'parse_oper.c', diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index f61f794755..b44ff44991 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -697,7 +697,11 @@ transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf) char **names; int colno; - /* Currently only XMLTABLE is supported */ + /* + * Currently we only support XMLTABLE here. See transformJsonTable() for + * JSON_TABLE support. + */ + tf->functype = TFT_XMLTABLE; constructName = "XMLTABLE"; docType = XMLOID; @@ -1104,13 +1108,17 @@ transformFromClauseItem(ParseState *pstate, Node *n, rtr->rtindex = nsitem->p_rtindex; return (Node *) rtr; } - else if (IsA(n, RangeTableFunc)) + else if (IsA(n, RangeTableFunc) || IsA(n, JsonTable)) { /* table function is like a plain relation */ RangeTblRef *rtr; ParseNamespaceItem *nsitem; - nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + if (IsA(n, RangeTableFunc)) + nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + else + nsitem = transformJsonTable(pstate, (JsonTable *) n); + *top_nsitem = nsitem; *namespace = list_make1(nsitem); rtr = makeNode(RangeTblRef); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index f6a7ba5ef5..21cad6d0c2 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4245,7 +4245,7 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) JsonFormatType format; char *constructName; - if (func->common->pathname) + if (func->common->pathname && func->op != JSON_TABLE_OP) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("JSON_TABLE path name is not allowed here"), @@ -4264,6 +4264,9 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) case JSON_EXISTS_OP: constructName = "JSON_EXISTS()"; break; + case JSON_TABLE_OP: + constructName = "JSON_TABLE()"; + break; default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); break; @@ -4302,14 +4305,19 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) transformJsonPassingArgs(pstate, format, func->common->passing, &jsexpr->passing_values, &jsexpr->passing_names); - if (func->op != JSON_EXISTS_OP) + if (func->op != JSON_EXISTS_OP && func->op != JSON_TABLE_OP) jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, JSON_BEHAVIOR_NULL); - jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, - func->op == JSON_EXISTS_OP ? - JSON_BEHAVIOR_FALSE : - JSON_BEHAVIOR_NULL); + if (func->op == JSON_EXISTS_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_FALSE); + else if (func->op == JSON_TABLE_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_EMPTY); + else + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_NULL); return jsexpr; } @@ -4610,6 +4618,21 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->result_coercion->expr = NULL; } break; + + case JSON_TABLE_OP: + jsexpr->returning = makeNode(JsonReturning); + jsexpr->returning->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + jsexpr->returning->typid = exprType(contextItemExpr); + jsexpr->returning->typmod = -1; + + if (jsexpr->returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("JSON_TABLE() is not yet implemented for the json type"), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + break; } if (exprType(contextItemExpr) != JSONBOID) diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c new file mode 100644 index 0000000000..a7802e0499 --- /dev/null +++ b/src/backend/parser/parse_jsontable.c @@ -0,0 +1,739 @@ +/*------------------------------------------------------------------------- + * + * parse_jsontable.c + * parsing of JSON_TABLE + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/parser/parse_jsontable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/pg_collation.h" +#include "catalog/pg_type.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "optimizer/optimizer.h" +#include "parser/parse_clause.h" +#include "parser/parse_collate.h" +#include "parser/parse_expr.h" +#include "parser/parse_relation.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/json.h" +#include "utils/lsyscache.h" + +/* Context for JSON_TABLE transformation */ +typedef struct JsonTableParseContext +{ + ParseState *pstate; /* parsing state */ + JsonTable *table; /* untransformed node */ + TableFunc *tablefunc; /* transformed node */ + List *pathNames; /* list of all path and columns names */ + int pathNameId; /* path name id counter */ + Oid contextItemTypid; /* type oid of context item (json/jsonb) */ +} JsonTableParseContext; + +static JsonTableParent *transformJsonTableColumns(JsonTableParseContext *cxt, + JsonTablePlan *plan, + List *columns, + char *pathSpec, + char **pathName, + int location); + +static Node * +makeStringConst(char *str, int location) +{ + A_Const *n = makeNode(A_Const); + + n->val.node.type = T_String; + n->val.sval.sval = str; + n->location = location; + + return (Node *) n; +} + +/* + * Transform JSON_TABLE column + * - regular column into JSON_VALUE() + * - FORMAT JSON column into JSON_QUERY() + * - EXISTS column into JSON_EXISTS() + */ +static Node * +transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, + List *passingArgs, bool errorOnError) +{ + JsonFuncExpr *jfexpr = makeNode(JsonFuncExpr); + JsonCommon *common = makeNode(JsonCommon); + JsonOutput *output = makeNode(JsonOutput); + char *pathspec; + JsonFormat *default_format; + + jfexpr->op = + jtc->coltype == JTC_REGULAR ? JSON_VALUE_OP : + jtc->coltype == JTC_EXISTS ? JSON_EXISTS_OP : JSON_QUERY_OP; + jfexpr->common = common; + jfexpr->output = output; + jfexpr->on_empty = jtc->on_empty; + jfexpr->on_error = jtc->on_error; + if (!jfexpr->on_error && errorOnError) + jfexpr->on_error = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); + jfexpr->omit_quotes = jtc->omit_quotes; + jfexpr->wrapper = jtc->wrapper; + jfexpr->location = jtc->location; + + output->typeName = jtc->typeName; + output->returning = makeNode(JsonReturning); + output->returning->format = jtc->format; + + default_format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + + common->pathname = NULL; + common->expr = makeJsonValueExpr((Expr *) contextItemExpr, default_format); + common->passing = passingArgs; + + if (jtc->pathspec) + pathspec = jtc->pathspec; + else + { + /* Construct default path as '$."column_name"' */ + StringInfoData path; + + initStringInfo(&path); + + appendStringInfoString(&path, "$."); + escape_json(&path, jtc->name); + + pathspec = path.data; + } + + common->pathspec = makeStringConst(pathspec, -1); + + return (Node *) jfexpr; +} + +static bool +isJsonTablePathNameDuplicate(JsonTableParseContext *cxt, const char *pathname) +{ + ListCell *lc; + + foreach(lc, cxt->pathNames) + { + if (!strcmp(pathname, (const char *) lfirst(lc))) + return true; + } + + return false; +} + +/* Register the column name in the path name list. */ +static void +registerJsonTableColumn(JsonTableParseContext *cxt, char *colname) +{ + if (isJsonTablePathNameDuplicate(cxt, colname)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_ALIAS), + errmsg("duplicate JSON_TABLE column name: %s", colname), + errhint("JSON_TABLE column names must be distinct from one another."))); + + cxt->pathNames = lappend(cxt->pathNames, colname); +} + +/* Recursively register all nested column names in the path name list. */ +static void +registerAllJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED) + { + if (jtc->pathname) + registerJsonTableColumn(cxt, jtc->pathname); + + registerAllJsonTableColumns(cxt, jtc->columns); + } + else + { + registerJsonTableColumn(cxt, jtc->name); + } + } +} + +/* Generate a new unique JSON_TABLE path name. */ +static char * +generateJsonTablePathName(JsonTableParseContext *cxt) +{ + char namebuf[32]; + char *name = namebuf; + + do + { + snprintf(namebuf, sizeof(namebuf), "json_table_path_%d", + ++cxt->pathNameId); + } while (isJsonTablePathNameDuplicate(cxt, name)); + + name = pstrdup(name); + cxt->pathNames = lappend(cxt->pathNames, name); + + return name; +} + +/* Collect sibling path names from plan to the specified list. */ +static void +collectSiblingPathsInJsonTablePlan(JsonTablePlan *plan, List **paths) +{ + if (plan->plan_type == JSTP_SIMPLE) + *paths = lappend(*paths, plan->pathname); + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + *paths = lappend(*paths, plan->plan1->pathname); + } + else if (plan->join_type == JSTPJ_CROSS || + plan->join_type == JSTPJ_UNION) + { + collectSiblingPathsInJsonTablePlan(plan->plan1, paths); + collectSiblingPathsInJsonTablePlan(plan->plan2, paths); + } + else + elog(ERROR, "invalid JSON_TABLE join type %d", + plan->join_type); + } +} + +/* + * Validate child JSON_TABLE plan by checking that: + * - all nested columns have path names specified + * - all nested columns have corresponding node in the sibling plan + * - plan does not contain duplicate or extra nodes + */ +static void +validateJsonTableChildPlan(ParseState *pstate, JsonTablePlan *plan, + List *columns) +{ + ListCell *lc1; + List *siblings = NIL; + int nchildren = 0; + + if (plan) + collectSiblingPathsInJsonTablePlan(plan, &siblings); + + foreach(lc1, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc1)); + + if (jtc->coltype == JTC_NESTED) + { + ListCell *lc2; + bool found = false; + + if (!jtc->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("nested JSON_TABLE columns must contain an explicit AS pathname specification if an explicit PLAN clause is used"), + parser_errposition(pstate, jtc->location))); + + /* find nested path name in the list of sibling path names */ + foreach(lc2, siblings) + { + if ((found = !strcmp(jtc->pathname, lfirst(lc2)))) + break; + } + + if (!found) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node for nested path %s was not found in plan.", jtc->pathname), + parser_errposition(pstate, jtc->location))); + + nchildren++; + } + } + + if (list_length(siblings) > nchildren) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node contains some extra or duplicate sibling nodes."), + parser_errposition(pstate, plan ? plan->location : -1))); +} + +static JsonTableColumn * +findNestedJsonTableColumn(List *columns, const char *pathname) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED && + jtc->pathname && + !strcmp(jtc->pathname, pathname)) + return jtc; + } + + return NULL; +} + +static Node * +transformNestedJsonTableColumn(JsonTableParseContext *cxt, JsonTableColumn *jtc, + JsonTablePlan *plan) +{ + JsonTableParent *node; + char *pathname = jtc->pathname; + + node = transformJsonTableColumns(cxt, plan, jtc->columns, jtc->pathspec, + &pathname, jtc->location); + + return (Node *) node; +} + +static Node * +makeJsonTableSiblingJoin(bool cross, Node *lnode, Node *rnode) +{ + JsonTableSibling *join = makeNode(JsonTableSibling); + + join->larg = lnode; + join->rarg = rnode; + join->cross = cross; + + return (Node *) join; +} + +/* + * Recursively transform child JSON_TABLE plan. + * + * Default plan is transformed into a cross/union join of its nested columns. + * Simple and outer/inner plans are transformed into a JsonTableParent by + * finding and transforming corresponding nested column. + * Sibling plans are recursively transformed into a JsonTableSibling. + */ +static Node * +transformJsonTableChildPlan(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns) +{ + JsonTableColumn *jtc = NULL; + + if (!plan || plan->plan_type == JSTP_DEFAULT) + { + /* unspecified or default plan */ + Node *res = NULL; + ListCell *lc; + bool cross = plan && (plan->join_type & JSTPJ_CROSS); + + /* transform all nested columns into cross/union join */ + foreach(lc, columns) + { + JsonTableColumn *col = castNode(JsonTableColumn, lfirst(lc)); + Node *node; + + if (col->coltype != JTC_NESTED) + continue; + + node = transformNestedJsonTableColumn(cxt, col, plan); + + /* join transformed node with previous sibling nodes */ + res = res ? makeJsonTableSiblingJoin(cross, res, node) : node; + } + + return res; + } + else if (plan->plan_type == JSTP_SIMPLE) + { + jtc = findNestedJsonTableColumn(columns, plan->pathname); + } + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + jtc = findNestedJsonTableColumn(columns, plan->plan1->pathname); + } + else + { + Node *node1 = transformJsonTableChildPlan(cxt, plan->plan1, + columns); + Node *node2 = transformJsonTableChildPlan(cxt, plan->plan2, + columns); + + return makeJsonTableSiblingJoin(plan->join_type == JSTPJ_CROSS, + node1, node2); + } + } + else + elog(ERROR, "invalid JSON_TABLE plan type %d", plan->plan_type); + + if (!jtc) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name was %s not found in nested columns list.", + plan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + return transformNestedJsonTableColumn(cxt, jtc, plan); +} + +/* Check whether type is json/jsonb, array, or record. */ +static bool +typeIsComposite(Oid typid) +{ + char typtype; + + if (typid == JSONOID || + typid == JSONBOID || + typid == RECORDOID || + type_is_array(typid)) + return true; + + typtype = get_typtype(typid); + + if (typtype == TYPTYPE_COMPOSITE) + return true; + + if (typtype == TYPTYPE_DOMAIN) + return typeIsComposite(getBaseType(typid)); + + return false; +} + +/* Append transformed non-nested JSON_TABLE columns to the TableFunc node */ +static void +appendJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *col; + ParseState *pstate = cxt->pstate; + JsonTable *jt = cxt->table; + TableFunc *tf = cxt->tablefunc; + bool errorOnError = jt->on_error && + jt->on_error->btype == JSON_BEHAVIOR_ERROR; + + foreach(col, columns) + { + JsonTableColumn *rawc = castNode(JsonTableColumn, lfirst(col)); + Oid typid; + int32 typmod; + Node *colexpr; + + if (rawc->name) + { + /* make sure column names are unique */ + ListCell *colname; + + foreach(colname, tf->colnames) + if (!strcmp((const char *) colname, rawc->name)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("column name \"%s\" is not unique", + rawc->name), + parser_errposition(pstate, rawc->location))); + + tf->colnames = lappend(tf->colnames, + makeString(pstrdup(rawc->name))); + } + + /* + * Determine the type and typmod for the new column. FOR ORDINALITY + * columns are INTEGER by standard; the others are user-specified. + */ + switch (rawc->coltype) + { + case JTC_FOR_ORDINALITY: + colexpr = NULL; + typid = INT4OID; + typmod = -1; + break; + + case JTC_REGULAR: + typenameTypeIdAndMod(pstate, rawc->typeName, &typid, &typmod); + + /* + * Use implicit FORMAT JSON for composite types (arrays and + * records) + */ + if (typeIsComposite(typid)) + rawc->coltype = JTC_FORMATTED; + else if (rawc->wrapper != JSW_NONE) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use WITH WRAPPER clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + else if (rawc->omit_quotes) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use OMIT QUOTES clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + + /* FALLTHROUGH */ + case JTC_EXISTS: + case JTC_FORMATTED: + { + Node *je; + CaseTestExpr *param = makeNode(CaseTestExpr); + + param->collation = InvalidOid; + param->typeId = cxt->contextItemTypid; + param->typeMod = -1; + + je = transformJsonTableColumn(rawc, (Node *) param, + NIL, errorOnError); + + colexpr = transformExpr(pstate, je, EXPR_KIND_FROM_FUNCTION); + assign_expr_collations(pstate, colexpr); + + typid = exprType(colexpr); + typmod = exprTypmod(colexpr); + break; + } + + case JTC_NESTED: + continue; + + default: + elog(ERROR, "unknown JSON_TABLE column type: %d", rawc->coltype); + break; + } + + tf->coltypes = lappend_oid(tf->coltypes, typid); + tf->coltypmods = lappend_int(tf->coltypmods, typmod); + tf->colcollations = lappend_oid(tf->colcollations, get_typcollation(typid)); + tf->colvalexprs = lappend(tf->colvalexprs, colexpr); + } +} + +/* + * Create transformed JSON_TABLE parent plan node by appending all non-nested + * columns to the TableFunc node and remembering their indices in the + * colvalexprs list. + */ +static JsonTableParent * +makeParentJsonTableNode(JsonTableParseContext *cxt, char *pathSpec, char *pathName, + List *columns) +{ + JsonTableParent *node = makeNode(JsonTableParent); + + node->path = makeNode(JsonTablePath); + node->path->value = makeConst(JSONPATHOID, -1, InvalidOid, -1, + DirectFunctionCall1(jsonpath_in, + CStringGetDatum(pathSpec)), + false, false); + if (pathName) + node->path->name = pstrdup(pathName); + + /* save start of column range */ + node->colMin = list_length(cxt->tablefunc->colvalexprs); + + appendJsonTableColumns(cxt, columns); + + /* save end of column range */ + node->colMax = list_length(cxt->tablefunc->colvalexprs) - 1; + + node->errorOnError = + cxt->table->on_error && + cxt->table->on_error->btype == JSON_BEHAVIOR_ERROR; + + return node; +} + +static JsonTableParent * +transformJsonTableColumns(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns, char *pathSpec, char **pathName, + int location) +{ + JsonTableParent *node; + JsonTablePlan *childPlan; + bool defaultPlan = !plan || plan->plan_type == JSTP_DEFAULT; + + if (!*pathName) + { + if (cxt->table->plan) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE expression"), + errdetail("JSON_TABLE columns must contain " + "explicit AS pathname specification if " + "explicit PLAN clause is used"), + parser_errposition(cxt->pstate, location))); + + *pathName = generateJsonTablePathName(cxt); + } + + if (defaultPlan) + childPlan = plan; + else + { + /* validate parent and child plans */ + JsonTablePlan *parentPlan; + + if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type != JSTPJ_INNER && + plan->join_type != JSTPJ_OUTER) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Expected INNER or OUTER JSON_TABLE plan node."), + parser_errposition(cxt->pstate, plan->location))); + + parentPlan = plan->plan1; + childPlan = plan->plan2; + + Assert(parentPlan->plan_type != JSTP_JOINED); + Assert(parentPlan->pathname); + } + else + { + parentPlan = plan; + childPlan = NULL; + } + + if (strcmp(parentPlan->pathname, *pathName)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name mismatch: expected %s but %s is given.", + *pathName, parentPlan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + validateJsonTableChildPlan(cxt->pstate, childPlan, columns); + } + + /* transform only non-nested columns */ + node = makeParentJsonTableNode(cxt, pathSpec, *pathName, columns); + + if (childPlan || defaultPlan) + { + /* transform recursively nested columns */ + node->child = transformJsonTableChildPlan(cxt, childPlan, columns); + if (node->child) + node->outerJoin = !plan || (plan->join_type & JSTPJ_OUTER); + /* else: default plan case, no children found */ + } + + return node; +} + +/* + * transformJsonTable - + * Transform a raw JsonTable into TableFunc. + * + * Transform the document-generating expression, the row-generating expression, + * the column-generating expressions, and the default value expressions. + */ +ParseNamespaceItem * +transformJsonTable(ParseState *pstate, JsonTable *jt) +{ + JsonTableParseContext cxt; + TableFunc *tf = makeNode(TableFunc); + JsonFuncExpr *jfe = makeNode(JsonFuncExpr); + JsonExpr *je; + JsonTablePlan *plan = jt->plan; + JsonCommon *jscommon; + char *rootPathName = jt->common->pathname; + char *rootPath; + bool is_lateral; + + cxt.pstate = pstate; + cxt.table = jt; + cxt.tablefunc = tf; + cxt.pathNames = NIL; + cxt.pathNameId = 0; + + if (rootPathName) + registerJsonTableColumn(&cxt, rootPathName); + + registerAllJsonTableColumns(&cxt, jt->columns); + +#if 0 /* XXX it' unclear from the standard whether + * root path name is mandatory or not */ + if (plan && plan->plan_type != JSTP_DEFAULT && !rootPathName) + { + /* Assign root path name and create corresponding plan node */ + JsonTablePlan *rootNode = makeNode(JsonTablePlan); + JsonTablePlan *rootPlan = (JsonTablePlan *) + makeJsonTableJoinedPlan(JSTPJ_OUTER, (Node *) rootNode, + (Node *) plan, jt->location); + + rootPathName = generateJsonTablePathName(&cxt); + + rootNode->plan_type = JSTP_SIMPLE; + rootNode->pathname = rootPathName; + + plan = rootPlan; + } +#endif + + jscommon = copyObject(jt->common); + jscommon->pathspec = makeStringConst(pstrdup("$"), -1); + + jfe->op = JSON_TABLE_OP; + jfe->common = jscommon; + jfe->on_error = jt->on_error; + jfe->location = jt->common->location; + + /* + * We make lateral_only names of this level visible, whether or not the + * RangeTableFunc is explicitly marked LATERAL. This is needed for SQL + * spec compliance and seems useful on convenience grounds for all + * functions in FROM. + * + * (LATERAL can't nest within a single pstate level, so we don't need + * save/restore logic here.) + */ + Assert(!pstate->p_lateral_active); + pstate->p_lateral_active = true; + + tf->functype = TFT_JSON_TABLE; + tf->docexpr = transformExpr(pstate, (Node *) jfe, EXPR_KIND_FROM_FUNCTION); + + cxt.contextItemTypid = exprType(tf->docexpr); + + if (!IsA(jt->common->pathspec, A_Const) || + castNode(A_Const, jt->common->pathspec)->val.node.type != T_String) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("only string constants supported in JSON_TABLE path specification"), + parser_errposition(pstate, + exprLocation(jt->common->pathspec)))); + + rootPath = castNode(A_Const, jt->common->pathspec)->val.sval.sval; + + tf->plan = (Node *) transformJsonTableColumns(&cxt, plan, jt->columns, + rootPath, &rootPathName, + jt->common->location); + + /* Also save a copy of the PASSING arguments in the TableFunc node. */ + je = (JsonExpr *) tf->docexpr; + tf->passingvalexprs = copyObject(je->passing_values); + + tf->ordinalitycol = -1; /* undefine ordinality column number */ + tf->location = jt->location; + + pstate->p_lateral_active = false; + + /* + * Mark the RTE as LATERAL if the user said LATERAL explicitly, or if + * there are any lateral cross-references in it. + */ + is_lateral = jt->lateral || contain_vars_of_level((Node *) tf, 0); + + return addRangeTableEntryForTableFunc(pstate, + tf, jt->alias, is_lateral, true); +} diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 41d60494b9..7da42c4772 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -2072,7 +2072,8 @@ addRangeTableEntryForTableFunc(ParseState *pstate, Assert(list_length(tf->coltypmods) == list_length(tf->colnames)); Assert(list_length(tf->colcollations) == list_length(tf->colnames)); - refname = alias ? alias->aliasname : pstrdup("xmltable"); + refname = alias ? alias->aliasname : + pstrdup(tf->functype == TFT_XMLTABLE ? "xmltable" : "json_table"); rte->rtekind = RTE_TABLEFUNC; rte->relid = InvalidOid; @@ -2095,7 +2096,7 @@ addRangeTableEntryForTableFunc(ParseState *pstate, ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), errmsg("%s function has %d columns available but %d columns specified", - "XMLTABLE", + tf->functype == TFT_XMLTABLE ? "XMLTABLE" : "JSON_TABLE", list_length(tf->colnames), numaliases))); rte->eref = eref; diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4f94fc69d6..6500e42485 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1992,6 +1992,9 @@ FigureColnameInternal(Node *node, char **name) case JSON_EXISTS_OP: *name = "json_exists"; return 2; + case JSON_TABLE_OP: + *name = "json_table"; + return 2; } break; default: diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index eded22e194..92d76d5cde 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -61,10 +61,12 @@ #include "catalog/pg_collation.h" #include "catalog/pg_type.h" +#include "executor/execExpr.h" #include "funcapi.h" #include "lib/stringinfo.h" #include "miscadmin.h" #include "nodes/miscnodes.h" +#include "nodes/nodeFuncs.h" #include "regex/regex.h" #include "utils/builtins.h" #include "utils/date.h" @@ -75,6 +77,8 @@ #include "utils/guc.h" #include "utils/json.h" #include "utils/jsonpath.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" #include "utils/timestamp.h" #include "utils/varlena.h" @@ -156,6 +160,61 @@ typedef struct JsonValueListIterator ListCell *next; } JsonValueListIterator; +/* Structures for JSON_TABLE execution */ + +typedef enum JsonTablePlanStateType +{ + JSON_TABLE_SCAN_STATE = 0, + JSON_TABLE_JOIN_STATE +} JsonTablePlanStateType; + +typedef struct JsonTablePlanState +{ + JsonTablePlanStateType type; + + struct JsonTablePlanState *parent; + struct JsonTablePlanState *nested; +} JsonTablePlanState; + +typedef struct JsonTableScanState +{ + JsonTablePlanState plan; + + MemoryContext mcxt; + JsonPath *path; + List *args; + JsonValueList found; + JsonValueListIterator iter; + Datum current; + int ordinal; + bool currentIsNull; + bool outerJoin; + bool errorOnError; + bool advanceNested; + bool reset; +} JsonTableScanState; + +typedef struct JsonTableJoinState +{ + JsonTablePlanState plan; + + JsonTablePlanState *left; + JsonTablePlanState *right; + bool cross; + bool advanceRight; +} JsonTableJoinState; + +/* random number to identify JsonTableExecContext */ +#define JSON_TABLE_EXEC_CONTEXT_MAGIC 418352867 + +typedef struct JsonTableExecContext +{ + int magic; + JsonTableScanState **colexprscans; + JsonTableScanState *root; + bool empty; +} JsonTableExecContext; + /* strict/lax flags is decomposed into four [un]wrap/error flags */ #define jspStrictAbsenseOfErrors(cxt) (!(cxt)->laxMode) #define jspAutoUnwrap(cxt) ((cxt)->laxMode) @@ -248,6 +307,7 @@ static JsonPathExecResult getArrayIndex(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbValue *jb, int32 *index); static JsonBaseObjectInfo setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id); +static void JsonValueListClear(JsonValueList *jvl); static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv); static int JsonValueListLength(const JsonValueList *jvl); static bool JsonValueListIsEmpty(JsonValueList *jvl); @@ -265,6 +325,14 @@ static JsonbValue *wrapItemsInArray(const JsonValueList *items); static int compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, bool useTz, bool *cast_error); + +static JsonTablePlanState *JsonTableInitPlanState(JsonTableExecContext *cxt, + Node *plan, + JsonTablePlanState *parent); +static bool JsonTablePlanNextRow(JsonTablePlanState *state); +static bool JsonTableScanNextRow(JsonTableScanState *scan); + + /****************** User interface to JsonPath executor ********************/ /* @@ -2518,6 +2586,13 @@ setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id) return baseObject; } +static void +JsonValueListClear(JsonValueList *jvl) +{ + jvl->singleton = NULL; + jvl->list = NULL; +} + static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv) { @@ -3123,3 +3198,471 @@ JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); } } + +/************************ JSON_TABLE functions ***************************/ + +/* + * Returns private data from executor state. Ensure validity by check with + * MAGIC number. + */ +static inline JsonTableExecContext * +GetJsonTableExecContext(TableFuncScanState *state, const char *fname) +{ + JsonTableExecContext *result; + + if (!IsA(state, TableFuncScanState)) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + result = (JsonTableExecContext *) state->opaque; + if (result->magic != JSON_TABLE_EXEC_CONTEXT_MAGIC) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + + return result; +} + +/* Recursively initialize JSON_TABLE scan / join state */ +static JsonTableJoinState * +JsonTableInitJoinState(JsonTableExecContext *cxt, JsonTableSibling *plan, + JsonTablePlanState *parent) +{ + JsonTableJoinState *join = palloc0(sizeof(*join)); + + join->plan.type = JSON_TABLE_JOIN_STATE; + /* parent and nested not set. */ + + join->cross = plan->cross; + join->left = JsonTableInitPlanState(cxt, plan->larg, parent); + join->right = JsonTableInitPlanState(cxt, plan->rarg, parent); + + return join; +} + +static JsonTableScanState * +JsonTableInitScanState(JsonTableExecContext *cxt, JsonTableParent *plan, + JsonTablePlanState *parent, + List *args, MemoryContext mcxt) +{ + JsonTableScanState *scan = palloc0(sizeof(*scan)); + int i; + + scan->plan.type = JSON_TABLE_SCAN_STATE; + scan->plan.parent = parent; + + scan->outerJoin = plan->outerJoin; + scan->errorOnError = plan->errorOnError; + scan->path = DatumGetJsonPathP(plan->path->value->constvalue); + scan->args = args; + scan->mcxt = AllocSetContextCreate(mcxt, "JsonTableExecContext", + ALLOCSET_DEFAULT_SIZES); + + /* + * Set after settings scan->args and scan->mcxt, because the recursive call + * wants to use those values. + */ + scan->plan.nested = plan->child ? + JsonTableInitPlanState(cxt, plan->child, (JsonTablePlanState *) scan) : + NULL; + + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + + for (i = plan->colMin; i <= plan->colMax; i++) + cxt->colexprscans[i] = scan; + + return scan; +} + +/* Recursively initialize JSON_TABLE scan state */ +static JsonTablePlanState * +JsonTableInitPlanState(JsonTableExecContext *cxt, Node *plan, + JsonTablePlanState *parent) +{ + JsonTablePlanState *state; + + if (IsA(plan, JsonTableSibling)) + { + JsonTableSibling *join = castNode(JsonTableSibling, plan); + + state = (JsonTablePlanState *) + JsonTableInitJoinState(cxt, join, parent); + } + else + { + JsonTableParent *scan = castNode(JsonTableParent, plan); + JsonTableScanState *parent_scan = (JsonTableScanState *) parent; + + Assert(parent_scan); + state = (JsonTablePlanState *) + JsonTableInitScanState(cxt, scan, parent, parent_scan->args, + parent_scan->mcxt); + } + + return state; +} + +/* + * JsonTableInitOpaque + * Fill in TableFuncScanState->opaque for JsonTable processor + */ +static void +JsonTableInitOpaque(TableFuncScanState *state, int natts) +{ + JsonTableExecContext *cxt; + PlanState *ps = &state->ss.ps; + TableFuncScan *tfs = castNode(TableFuncScan, ps->plan); + TableFunc *tf = tfs->tablefunc; + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + JsonExpr *je = castNode(JsonExpr, tf->docexpr); + List *args = NIL; + + cxt = palloc0(sizeof(JsonTableExecContext)); + cxt->magic = JSON_TABLE_EXEC_CONTEXT_MAGIC; + + if (state->passingvalexprs) + { + ListCell *exprlc; + ListCell *namelc; + + Assert(list_length(state->passingvalexprs) == + list_length(je->passing_names)); + forboth(exprlc, state->passingvalexprs, + namelc, je->passing_names) + { + ExprState *state = lfirst_node(ExprState, exprlc); + String *name = lfirst_node(String, namelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(name->sval); + var->typid = exprType((Node *) state->expr); + var->typmod = exprTypmod((Node *) state->expr); + + /* + * Evaluate the expression and save the value to be returned by + * GetJsonPathVar(). + */ + var->value = ExecEvalExpr(state, ps->ps_ExprContext, + &var->isnull); + + args = lappend(args, var); + } + } + + cxt->colexprscans = palloc(sizeof(JsonTableScanState *) * + list_length(tf->colvalexprs)); + + cxt->root = JsonTableInitScanState(cxt, root, NULL, args, + CurrentMemoryContext); + state->opaque = cxt; +} + +/* Reset scan iterator to the beginning of the item list */ +static void +JsonTableRescan(JsonTableScanState *scan) +{ + JsonValueListInitIterator(&scan->found, &scan->iter); + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + scan->advanceNested = false; + scan->ordinal = 0; +} + +/* Reset context item of a scan, execute JSON path and reset a scan */ +static void +JsonTableResetContextItem(JsonTableScanState *scan, Datum item) +{ + MemoryContext oldcxt; + JsonPathExecResult res; + Jsonb *js = (Jsonb *) DatumGetJsonbP(item); + + JsonValueListClear(&scan->found); + + MemoryContextResetOnly(scan->mcxt); + + oldcxt = MemoryContextSwitchTo(scan->mcxt); + + res = executeJsonPath(scan->path, scan->args, GetJsonPathVar, js, + scan->errorOnError, &scan->found, + false /* FIXME */ ); + + MemoryContextSwitchTo(oldcxt); + + if (jperIsError(res)) + { + Assert(!scan->errorOnError); + JsonValueListClear(&scan->found); /* EMPTY ON ERROR case */ + } + + JsonTableRescan(scan); +} + +/* + * JsonTableSetDocument + * Install the input document + */ +static void +JsonTableSetDocument(TableFuncScanState *state, Datum value) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableSetDocument"); + + JsonTableResetContextItem(cxt->root, value); +} + +/* Recursively reset scan and its child nodes */ +static void +JsonTableRescanRecursive(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTableRescanRecursive(join->left); + JsonTableRescanRecursive(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan = (JsonTableScanState *) state; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + JsonTableRescan(scan); + if (scan->plan.nested) + JsonTableRescanRecursive(scan->plan.nested); + } +} + +/* + * Fetch next row from a cross/union joined scan. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTablePlanNextRow(JsonTablePlanState *state) +{ + JsonTableJoinState *join; + + if (state->type == JSON_TABLE_SCAN_STATE) + return JsonTableScanNextRow((JsonTableScanState *) state); + + join = (JsonTableJoinState *) state; + if (join->advanceRight) + { + /* fetch next inner row */ + if (JsonTablePlanNextRow(join->right)) + return true; + + /* inner rows are exhausted */ + if (join->cross) + join->advanceRight = false; /* next outer row */ + else + return false; /* end of scan */ + } + + while (!join->advanceRight) + { + /* fetch next outer row */ + bool left = JsonTablePlanNextRow(join->left); + + if (join->cross) + { + if (!left) + return false; /* end of scan */ + + JsonTableRescanRecursive(join->right); + + if (!JsonTablePlanNextRow(join->right)) + continue; /* next outer row */ + + join->advanceRight = true; /* next inner row */ + } + else if (!left) + { + if (!JsonTablePlanNextRow(join->right)) + return false; /* end of scan */ + + join->advanceRight = true; /* next inner row */ + } + + break; + } + + return true; +} + +/* Recursively set 'reset' flag of scan and its child nodes */ +static void +JsonTablePlanReset(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTablePlanReset(join->left); + JsonTablePlanReset(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + scan = (JsonTableScanState *) state; + scan->reset = true; + scan->advanceNested = false; + + if (scan->plan.nested) + JsonTablePlanReset(scan->plan.nested); + } +} + +/* + * Fetch next row from a simple scan with outer/inner joined nested subscans. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTableScanNextRow(JsonTableScanState *scan) +{ + /* reset context item if requested */ + if (scan->reset) + { + JsonTableScanState *parent_scan = + (JsonTableScanState *) scan->plan.parent; + + Assert(parent_scan && !parent_scan->currentIsNull); + JsonTableResetContextItem(scan, parent_scan->current); + scan->reset = false; + } + + if (scan->advanceNested) + { + /* fetch next nested row */ + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested) + return true; + } + + for (;;) + { + /* fetch next row */ + JsonbValue *jbv = JsonValueListNext(&scan->found, &scan->iter); + MemoryContext oldcxt; + + if (!jbv) + { + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + return false; /* end of scan */ + } + + /* set current row item */ + oldcxt = MemoryContextSwitchTo(scan->mcxt); + scan->current = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + scan->currentIsNull = false; + MemoryContextSwitchTo(oldcxt); + + scan->ordinal++; + + if (!scan->plan.nested) + break; + + JsonTablePlanReset(scan->plan.nested); + + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested || scan->outerJoin) + break; + } + + return true; +} + +/* + * JsonTableFetchRow + * Prepare the next "current" tuple for upcoming GetValue calls. + * Returns FALSE if the row-filter expression returned no more rows. + */ +static bool +JsonTableFetchRow(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableFetchRow"); + + if (cxt->empty) + return false; + + return JsonTableScanNextRow(cxt->root); +} + +/* + * JsonTableGetValue + * Return the value for column number 'colnum' for the current row. + * + * This leaks memory, so be sure to reset often the context in which it's + * called. + */ +static Datum +JsonTableGetValue(TableFuncScanState *state, int colnum, + Oid typid, int32 typmod, bool *isnull) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableGetValue"); + ExprContext *econtext = state->ss.ps.ps_ExprContext; + ExprState *estate = list_nth(state->colvalexprs, colnum); + JsonTableScanState *scan = cxt->colexprscans[colnum]; + Datum result; + + if (scan->currentIsNull) /* NULL from outer/union join */ + { + result = (Datum) 0; + *isnull = true; + } + else if (estate) /* regular column */ + { + Datum saved_caseValue = econtext->caseValue_datum; + bool saved_caseIsNull = econtext->caseValue_isNull; + + /* Pass the value for CaseTestExpr that may be present in colexpr */ + econtext->caseValue_datum = scan->current; + econtext->caseValue_isNull = false; + + result = ExecEvalExpr(estate, econtext, isnull); + + econtext->caseValue_datum = saved_caseValue; + econtext->caseValue_isNull = saved_caseIsNull; + } + else + { + result = Int32GetDatum(scan->ordinal); /* ordinality column */ + *isnull = false; + } + + return result; +} + +/* + * JsonTableDestroyOpaque + */ +static void +JsonTableDestroyOpaque(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableDestroyOpaque"); + + /* not valid anymore */ + cxt->magic = 0; + + state->opaque = NULL; +} + +const TableFuncRoutine JsonbTableRoutine = +{ + JsonTableInitOpaque, + JsonTableSetDocument, + NULL, + NULL, + NULL, + JsonTableFetchRow, + JsonTableGetValue, + JsonTableDestroyOpaque +}; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d1ec418ccf..e89d1e03d6 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -522,6 +522,8 @@ static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); static void get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit); +static void get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8606,7 +8608,8 @@ get_json_behavior(JsonBehavior *behavior, deparse_context *context, /* * get_json_expr_options * - * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS and + * JSON_TABLE columns. */ static void get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, @@ -9854,6 +9857,9 @@ get_rule_expr(Node *node, deparse_context *context, case JSON_EXISTS_OP: appendStringInfoString(buf, "JSON_EXISTS("); break; + default: + elog(ERROR, "unexpected JsonExpr type: %d", jexpr->op); + break; } get_rule_expr(jexpr->formatted_expr, context, showimplicit); @@ -11207,16 +11213,14 @@ get_sublink_expr(SubLink *sublink, deparse_context *context) /* ---------- - * get_tablefunc - Parse back a table function + * get_xmltable - Parse back a XMLTABLE function * ---------- */ static void -get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +get_xmltable(TableFunc *tf, deparse_context *context, bool showimplicit) { StringInfo buf = context->buf; - /* XMLTABLE is the only existing implementation. */ - appendStringInfoString(buf, "XMLTABLE("); if (tf->ns_uris != NIL) @@ -11307,6 +11311,271 @@ get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) appendStringInfoChar(buf, ')'); } +/* + * get_json_nested_columns - Parse back nested JSON_TABLE columns + */ +static void +get_json_table_nested_columns(TableFunc *tf, Node *node, + deparse_context *context, bool showimplicit, + bool needcomma) +{ + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_nested_columns(tf, n->larg, context, showimplicit, + needcomma); + get_json_table_nested_columns(tf, n->rarg, context, showimplicit, true); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + if (needcomma) + appendStringInfoChar(context->buf, ','); + + appendStringInfoChar(context->buf, ' '); + appendContextKeyword(context, "NESTED PATH ", 0, 0, 0); + get_const_expr(n->path->value, context, -1); + appendStringInfo(context->buf, " AS %s", quote_identifier(n->path->name)); + get_json_table_columns(tf, n, context, showimplicit); + } +} + +/* + * get_json_table_plan - Parse back a JSON_TABLE plan + */ +static void +get_json_table_plan(TableFunc *tf, Node *node, deparse_context *context, + bool parenthesize) +{ + if (parenthesize) + appendStringInfoChar(context->buf, '('); + + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_plan(tf, n->larg, context, + IsA(n->larg, JsonTableSibling) || + castNode(JsonTableParent, n->larg)->child); + + appendStringInfoString(context->buf, n->cross ? " CROSS " : " UNION "); + + get_json_table_plan(tf, n->rarg, context, + IsA(n->rarg, JsonTableSibling) || + castNode(JsonTableParent, n->rarg)->child); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + appendStringInfoString(context->buf, quote_identifier(n->path->name)); + + if (n->child) + { + appendStringInfoString(context->buf, + n->outerJoin ? " OUTER " : " INNER "); + get_json_table_plan(tf, n->child, context, + IsA(n->child, JsonTableSibling)); + } + } + + if (parenthesize) + appendStringInfoChar(context->buf, ')'); +} + +/* + * get_json_table_columns - Parse back JSON_TABLE columns + */ +static void +get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + ListCell *lc_colname; + ListCell *lc_coltype; + ListCell *lc_coltypmod; + ListCell *lc_colvalexpr; + int colnum = 0; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "COLUMNS (", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forfour(lc_colname, tf->colnames, + lc_coltype, tf->coltypes, + lc_coltypmod, tf->coltypmods, + lc_colvalexpr, tf->colvalexprs) + { + char *colname = strVal(lfirst(lc_colname)); + JsonExpr *colexpr; + Oid typid; + int32 typmod; + bool ordinality; + JsonBehaviorType default_behavior; + + typid = lfirst_oid(lc_coltype); + typmod = lfirst_int(lc_coltypmod); + colexpr = castNode(JsonExpr, lfirst(lc_colvalexpr)); + + if (colnum < node->colMin) + { + colnum++; + continue; + } + + if (colnum > node->colMax) + break; + + if (colnum > node->colMin) + appendStringInfoString(buf, ", "); + + colnum++; + + ordinality = !colexpr; + + appendContextKeyword(context, "", 0, 0, 0); + + appendStringInfo(buf, "%s %s", quote_identifier(colname), + ordinality ? "FOR ORDINALITY" : + format_type_with_typemod(typid, typmod)); + if (ordinality) + continue; + + if (colexpr->op == JSON_EXISTS_OP) + { + appendStringInfoString(buf, " EXISTS"); + default_behavior = JSON_BEHAVIOR_FALSE; + } + else + { + if (colexpr->op == JSON_QUERY_OP) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(typid, &typcategory, &typispreferred); + + if (typcategory == TYPCATEGORY_STRING) + appendStringInfoString(buf, + colexpr->format->format_type == JS_FORMAT_JSONB ? + " FORMAT JSONB" : " FORMAT JSON"); + } + + default_behavior = JSON_BEHAVIOR_NULL; + } + + if (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR) + default_behavior = JSON_BEHAVIOR_ERROR; + + appendStringInfoString(buf, " PATH "); + + get_json_path_spec(colexpr->path_spec, context, showimplicit); + + get_json_expr_options(colexpr, context, default_behavior); + } + + if (node->child) + get_json_table_nested_columns(tf, node->child, context, showimplicit, + node->colMax >= node->colMin); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_json_table - Parse back a JSON_TABLE function + * ---------- + */ +static void +get_json_table(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + + appendStringInfoString(buf, "JSON_TABLE("); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_const_expr(root->path->value, context, -1); + + appendStringInfo(buf, " AS %s", quote_identifier(root->path->name)); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PASSING ", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr((Node *) lfirst(lc2), context, false); + appendStringInfo(buf, " AS %s", + quote_identifier((lfirst_node(String, lc1))->sval) + ); + } + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + } + + get_json_table_columns(tf, root, context, showimplicit); + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PLAN ", 0, 0, 0); + get_json_table_plan(tf, (Node *) root, context, true); + + if (jexpr->on_error->btype != JSON_BEHAVIOR_EMPTY) + get_json_behavior(jexpr->on_error, context, "ERROR"); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_tablefunc - Parse back a table function + * ---------- + */ +static void +get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + /* XMLTABLE and JSON_TABLE are the only existing implementations. */ + + if (tf->functype == TFT_XMLTABLE) + get_xmltable(tf, context, showimplicit); + else if (tf->functype == TFT_JSON_TABLE) + get_json_table(tf, context, showimplicit); +} + /* ---------- * get_from_clause - Parse back a FROM clause * diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 584bf7001a..91453681e3 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1879,6 +1879,8 @@ typedef struct TableFuncScanState ExprState *rowexpr; /* state for row-generating expression */ List *colexprs; /* state for column-generating expression */ List *coldefexprs; /* state for column default expressions */ + List *colvalexprs; /* state for column value expression */ + List *passingvalexprs; /* state for PASSING argument expression */ List *ns_names; /* same as TableFunc.ns_names */ List *ns_uris; /* list of states of namespace URI exprs */ Bitmapset *notnulls; /* nullability flag for each output column */ diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 5e149b0266..d8466a2699 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -112,6 +112,8 @@ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *expr, JsonFormat *format); extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); +extern Node *makeJsonTableJoinedPlan(JsonTablePlanJoinType type, + Node *plan1, Node *plan2, int location); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index bba2920aba..1a7093b83b 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1734,6 +1734,19 @@ typedef enum JsonQuotes */ typedef char *JsonPathSpec; +/* + * JsonTableColumnType - + * enumeration of JSON_TABLE column types + */ +typedef enum JsonTableColumnType +{ + JTC_FOR_ORDINALITY, + JTC_REGULAR, + JTC_EXISTS, + JTC_FORMATTED, + JTC_NESTED, +} JsonTableColumnType; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1787,6 +1800,83 @@ typedef struct JsonFuncExpr int location; /* token location, or -1 if unknown */ } JsonFuncExpr; +/* + * JsonTableColumn - + * untransformed representation of JSON_TABLE column + */ +typedef struct JsonTableColumn +{ + NodeTag type; + JsonTableColumnType coltype; /* column type */ + char *name; /* column name */ + TypeName *typeName; /* column type name */ + char *pathspec; /* path specification, if any */ + char *pathname; /* path name, if any */ + JsonFormat *format; /* JSON format clause, if specified */ + JsonWrapper wrapper; /* WRAPPER behavior for formatted columns */ + bool omit_quotes; /* omit or keep quotes on scalar strings? */ + List *columns; /* nested columns */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + int location; /* token location, or -1 if unknown */ +} JsonTableColumn; + +/* + * JsonTablePlanType - + * flags for JSON_TABLE plan node types representation + */ +typedef enum JsonTablePlanType +{ + JSTP_DEFAULT, + JSTP_SIMPLE, + JSTP_JOINED, +} JsonTablePlanType; + +/* + * JsonTablePlanJoinType - + * flags for JSON_TABLE join types representation + */ +typedef enum JsonTablePlanJoinType +{ + JSTPJ_INNER = 0x01, + JSTPJ_OUTER = 0x02, + JSTPJ_CROSS = 0x04, + JSTPJ_UNION = 0x08, +} JsonTablePlanJoinType; + +typedef struct JsonTablePlan JsonTablePlan; + +/* + * JsonTablePlan - + * untransformed representation of JSON_TABLE plan node + */ +struct JsonTablePlan +{ + NodeTag type; + JsonTablePlanType plan_type; /* plan type */ + JsonTablePlanJoinType join_type; /* join type (for joined plan only) */ + JsonTablePlan *plan1; /* first joined plan */ + JsonTablePlan *plan2; /* second joined plan */ + char *pathname; /* path name (for simple plan only) */ + int location; /* token location, or -1 if unknown */ +}; + +/* + * JsonTable - + * untransformed representation of JSON_TABLE + */ +typedef struct JsonTable +{ + NodeTag type; + JsonCommon *common; /* common JSON path syntax fields */ + List *columns; /* list of JsonTableColumn */ + JsonTablePlan *plan; /* join plan, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + Alias *alias; /* table alias in FROM clause */ + bool lateral; /* does it have LATERAL prefix? */ + int location; /* token location, or -1 if unknown */ +} JsonTable; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 0e5a160c90..926ab12b9e 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -86,8 +86,14 @@ typedef struct RangeVar int location; } RangeVar; +typedef enum TableFuncType +{ + TFT_XMLTABLE, + TFT_JSON_TABLE +} TableFuncType; + /* - * TableFunc - node for a table function, such as XMLTABLE. + * TableFunc - node for a table function, such as XMLTABLE or JSON_TABLE. * * Entries in the ns_names list are either String nodes containing * literal namespace names, or NULL pointers to represent DEFAULT. @@ -95,6 +101,8 @@ typedef struct RangeVar typedef struct TableFunc { NodeTag type; + /* XMLTABLE or JSON_TABLE */ + TableFuncType functype; /* list of namespace URI expressions */ List *ns_uris pg_node_attr(query_jumble_ignore); /* list of namespace names or NULL */ @@ -115,8 +123,14 @@ typedef struct TableFunc List *colexprs; /* list of column default expressions */ List *coldefexprs pg_node_attr(query_jumble_ignore); + /* list of column value expressions */ + List *colvalexprs pg_node_attr(query_jumble_ignore); + /* list of PASSING argument expressions */ + List *passingvalexprs pg_node_attr(query_jumble_ignore); /* nullability flag for each output column */ Bitmapset *notnulls pg_node_attr(query_jumble_ignore); + /* JSON_TABLE plan */ + Node *plan pg_node_attr(query_jumble_ignore); /* counts from 0; -1 if none specified */ int ordinalitycol pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ @@ -1550,7 +1564,8 @@ typedef enum JsonExprOp { JSON_VALUE_OP, /* JSON_VALUE() */ JSON_QUERY_OP, /* JSON_QUERY() */ - JSON_EXISTS_OP /* JSON_EXISTS() */ + JSON_EXISTS_OP, /* JSON_EXISTS() */ + JSON_TABLE_OP /* JSON_TABLE() */ } JsonExprOp; /* @@ -1765,6 +1780,48 @@ typedef struct JsonExpr int location; /* token location, or -1 if unknown */ } JsonExpr; +/* + * JsonTablePath + * A JSON path expression to be computed as part of evaluating + * a JSON_TABLE plan node + */ +typedef struct JsonTablePath +{ + NodeTag type; + + Const *value; + char *name; +} JsonTablePath; + +/* + * JsonTableParent - + * transformed representation of parent JSON_TABLE plan node + */ +typedef struct JsonTableParent +{ + NodeTag type; + JsonTablePath *path; + Node *child; /* nested columns, if any */ + bool outerJoin; /* outer or inner join for nested columns? */ + int colMin; /* min column index in the resulting column + * list */ + int colMax; /* max column index in the resulting column + * list */ + bool errorOnError; /* ERROR/EMPTY ON ERROR behavior */ +} JsonTableParent; + +/* + * JsonTableSibling - + * transformed representation of joined sibling JSON_TABLE plan node + */ +typedef struct JsonTableSibling +{ + NodeTag type; + Node *larg; /* left join node */ + Node *rarg; /* right join node */ + bool cross; /* cross or union join? */ +} JsonTableSibling; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0954d9fc7b..f88a6c9ac6 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -242,6 +242,7 @@ PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_table", JSON_TABLE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) @@ -284,6 +285,7 @@ PG_KEYWORD("names", NAMES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("national", NATIONAL, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("natural", NATURAL, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("nchar", NCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("nested", NESTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("new", NEW, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("next", NEXT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("nfc", NFC, UNRESERVED_KEYWORD, BARE_LABEL) @@ -334,7 +336,9 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h index 8e26482db4..7152a8ef48 100644 --- a/src/include/parser/parse_clause.h +++ b/src/include/parser/parse_clause.h @@ -51,4 +51,7 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle, extern Index assignSortGroupRef(TargetEntry *tle, List *tlist); extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList); +/* functions in parse_jsontable.c */ +extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt); + #endif /* PARSE_CLAUSE_H */ diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index 4dae78a98c..b7ada77cb1 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -15,6 +15,7 @@ #define JSONPATH_H #include "fmgr.h" +#include "executor/tablefunc.h" #include "nodes/pg_list.h" #include "nodes/primnodes.h" #include "utils/jsonb.h" @@ -288,4 +289,6 @@ extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars); +extern PGDLLIMPORT const TableFuncRoutine JsonbTableRoutine; + #endif diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out index 8b87580752..995f267404 100644 --- a/src/test/regress/expected/json_sqljson.out +++ b/src/test/regress/expected/json_sqljson.out @@ -16,3 +16,9 @@ ERROR: JSON_QUERY() is not yet implemented for the json type LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); ^ HINT: Try casting the argument to jsonb +-- JSON_TABLE +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); +ERROR: JSON_TABLE() is not yet implemented for the json type +LINE 1: SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo ... + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out index 7eb8faf487..b235291ec9 100644 --- a/src/test/regress/expected/jsonb_sqljson.out +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -1032,3 +1032,1072 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.dateti ERROR: functions in index expression must be marked IMMUTABLE CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; +-- JSON_TABLE +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); +ERROR: syntax error at or near "(" +LINE 1: SELECT JSON_TABLE('[]', '$'); + ^ +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); +ERROR: syntax error at or near ")" +LINE 1: SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + ^ +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); +ERROR: JSON_TABLE function has 1 columns available but 2 columns specified +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + foo +----- +(0 rows) + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + item | foo +------+----- + 123 | +(1 row) + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + js | id | id2 | int | text | char(4) | bool | numeric | domain | js | jb | jst | jsc | jsv | jsb | jsbq | aaa | aaa1 | exists1 | exists2 | exists3 | exists4 | js2 | jsb2w | jsb2q | ia | ta | jba +---------------------------------------------------------------------------------------+----+-----+-----+---------+---------+------+---------+---------+--------------+--------------+--------------+------+------+--------------+--------------+-----+------+---------+---------+---------+---------+--------------+----------------+--------------+----+----+----- + 1 | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [] | | | | | | | | | | | | | | | | | | | | | | | | | | | + {} | 1 | 1 | | | | | | | {} | {} | {} | {} | {} | {} | {} | | | f | 0 | | false | {} | [{}] | {} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 2 | 2 | 1 | 1.23 | 1.23 | | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | | | f | 0 | | false | 1.23 | [1.23] | 1.23 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 3 | 3 | 2 | 2 | 2 | | 2 | 2 | "2" | "2" | "2" | "2" | "2" | "2" | 2 | | | f | 0 | | false | "2" | ["2"] | 2 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 4 | 4 | | aaaaaaa | aaaa | | | aaaaaaa | "aaaaaaa" | "aaaaaaa" | "aaaaaaa" | "aaa | "aaa | "aaaaaaa" | | | | f | 0 | | false | "aaaaaaa" | ["aaaaaaa"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 5 | 5 | | foo | foo | | | | "foo" | "foo" | "foo" | "foo | "foo | "foo" | | | | f | 0 | | false | "foo" | ["foo"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 6 | 6 | | | | | | | null | null | null | null | null | null | null | | | f | 0 | | false | null | [null] | null | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 7 | 7 | 0 | false | fals | f | | false | false | false | false | fals | fals | false | false | | | f | 0 | | false | false | [false] | false | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 8 | 8 | 1 | true | true | t | | true | true | true | true | true | true | true | true | | | f | 0 | | false | true | [true] | true | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 9 | 9 | | | | | | | {"aaa": 123} | {"aaa": 123} | {"aaa": 123} | {"aa | {"aa | {"aaa": 123} | {"aaa": 123} | 123 | 123 | t | 1 | 1 | true | {"aaa": 123} | [{"aaa": 123}] | {"aaa": 123} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 10 | 10 | | [1,2] | [1,2 | | | [1,2] | "[1,2]" | "[1,2]" | "[1,2]" | "[1, | "[1, | "[1,2]" | [1, 2] | | | f | 0 | | false | "[1,2]" | ["[1,2]"] | [1, 2] | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 11 | 11 | | "str" | "str | | | "str" | "\"str\"" | "\"str\"" | "\"str\"" | "\"s | "\"s | "\"str\"" | "str" | | | f | 0 | | false | "\"str\"" | ["\"str\""] | "str" | | | +(14 rows) + +-- JSON_TABLE: Test backward parsing +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); +\sv jsonb_table_view +CREATE OR REPLACE VIEW public.jsonb_table_view AS + SELECT id, + id2, + "int", + text, + "char(4)", + bool, + "numeric", + domain, + js, + jb, + jst, + jsc, + jsv, + jsb, + jsbq, + aaa, + aaa1, + exists1, + exists2, + exists3, + js2, + jsb2w, + jsb2q, + ia, + ta, + jba, + a1, + b1, + a11, + a21, + a22 + FROM JSON_TABLE( + 'null'::jsonb, '$[*]' AS json_table_path_1 + PASSING + 1 + 2 AS a, + '"foo"'::json AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, + "int" integer PATH '$', + text text PATH '$', + "char(4)" character(4) PATH '$', + bool boolean PATH '$', + "numeric" numeric PATH '$', + domain jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc character(4) FORMAT JSON PATH '$', + jsv character varying(4) FORMAT JSON PATH '$', + jsb jsonb PATH '$', + jsbq jsonb PATH '$' OMIT QUOTES, + aaa integer PATH '$."aaa"', + aaa1 integer PATH '$."aaa"', + exists1 boolean EXISTS PATH '$."aaa"', + exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia integer[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 + COLUMNS ( + a1 integer PATH '$."a1"', + b1 text PATH '$."b1"', + NESTED PATH '$[*]' AS "p1 1" + COLUMNS ( + a11 text PATH '$."a11"' + ) + ), + NESTED PATH '$[2]' AS p2 + COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" + COLUMNS ( + a21 text PATH '$."a21"' + ), + NESTED PATH '$[*]' AS p22 + COLUMNS ( + a22 text PATH '$."a22"' + ) + ) + ) + PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22)))) + ) +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Table Function Scan on "json_table" + Output: "json_table".id, "json_table".id2, "json_table"."int", "json_table".text, "json_table"."char(4)", "json_table".bool, "json_table"."numeric", "json_table".domain, "json_table".js, "json_table".jb, "json_table".jst, "json_table".jsc, "json_table".jsv, "json_table".jsb, "json_table".jsbq, "json_table".aaa, "json_table".aaa1, "json_table".exists1, "json_table".exists2, "json_table".exists3, "json_table".js2, "json_table".jsb2w, "json_table".jsb2q, "json_table".ia, "json_table".ta, "json_table".jba, "json_table".a1, "json_table".b1, "json_table".a11, "json_table".a21, "json_table".a22 + Table Function Call: JSON_TABLE('null'::jsonb, '$[*]' AS json_table_path_1 PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (id FOR ORDINALITY, id2 FOR ORDINALITY, "int" integer PATH '$', text text PATH '$', "char(4)" character(4) PATH '$', bool boolean PATH '$', "numeric" numeric PATH '$', domain jsonb_test_domain PATH '$', js json PATH '$', jb jsonb PATH '$', jst text FORMAT JSON PATH '$', jsc character(4) FORMAT JSON PATH '$', jsv character varying(4) FORMAT JSON PATH '$', jsb jsonb PATH '$', jsbq jsonb PATH '$' OMIT QUOTES, aaa integer PATH '$."aaa"', aaa1 integer PATH '$."aaa"', exists1 boolean EXISTS PATH '$."aaa"', exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, js2 json PATH '$', jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, jsb2q jsonb PATH '$' OMIT QUOTES, ia integer[] PATH '$', ta text[] PATH '$', jba jsonb[] PATH '$', NESTED PATH '$[1]' AS p1 COLUMNS (a1 integer PATH '$."a1"', b1 text PATH '$."b1"', NESTED PATH '$[*]' AS "p1 1" COLUMNS (a11 text PATH '$."a11"')), NESTED PATH '$[2]' AS p2 COLUMNS ( NESTED PATH '$[*]' AS "p2:1" COLUMNS (a21 text PATH '$."a21"'), NESTED PATH '$[*]' AS p22 COLUMNS (a22 text PATH '$."a22"'))) PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22))))) +(3 rows) + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + js | a +-------+--- + 1 | 1 + "err" | +(2 rows) + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; + a +--- + +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: jsonpath member accessor can only be applied to an object +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: no SQL/JSON item +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 1 +(1 row) + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); + a +--- + 0 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to smallint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to bigint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to real +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 E... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); + a +----- + fal +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to json +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to jsonb +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EX... + ^ +-- JSON_TABLE: nested paths and plans +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb '[]', '$' -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 4: NESTED PATH '$' COLUMNS ( -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: b +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +-- JSON_TABLE: plan validation +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p1) + ^ +DETAIL: Path name mismatch: expected p0 but p1 is given. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 UNION p1 UNION p11) + ^ +DETAIL: Expected INNER or OUTER JSON_TABLE plan node. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 8: NESTED PATH '$' AS p2 COLUMNS ( + ^ +DETAIL: Plan node for nested path p2 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 5: NESTED PATH '$' AS p11 COLUMNS ( foo int ), + ^ +DETAIL: Plan node for nested path p11 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) + ^ +DETAIL: Plan node contains some extra or duplicate sibling nodes. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 6: NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ^ +DETAIL: Plan node for nested path p12 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 9: NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ^ +DETAIL: Plan node for nested path p21 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + bar | foo | baz +-----+-----+----- +(0 rows) + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb 'null', 'strict $[*]' -- without root path name + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- JSON_TABLE: plan execution +CREATE TEMP TABLE jsonb_table_test (js jsonb); +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + n | a | c | b +---+----+----+--- + 1 | 1 | | + 2 | 2 | 10 | + 2 | 2 | | + 2 | 2 | 20 | + 2 | 2 | | 1 + 2 | 2 | | 2 + 2 | 2 | | 3 + 3 | 3 | | 1 + 3 | 3 | | 2 + 4 | -1 | | 1 + 4 | -1 | | 2 +(11 rows) + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + n | a | b | b1 | c | c1 | b +---+---+--------------+-----+------+----+----- + 1 | 1 | [1, 10] | 1 | 1 | | 101 + 1 | 1 | [1, 10] | 1 | null | | 101 + 1 | 1 | [1, 10] | 1 | 2 | | 101 + 1 | 1 | [1, 10] | 10 | 1 | | 110 + 1 | 1 | [1, 10] | 10 | null | | 110 + 1 | 1 | [1, 10] | 10 | 2 | | 110 + 1 | 1 | [2] | 2 | 1 | | 102 + 1 | 1 | [2] | 2 | null | | 102 + 1 | 1 | [2] | 2 | 2 | | 102 + 1 | 1 | [3, 30, 300] | 3 | 1 | | 103 + 1 | 1 | [3, 30, 300] | 3 | null | | 103 + 1 | 1 | [3, 30, 300] | 3 | 2 | | 103 + 1 | 1 | [3, 30, 300] | 30 | 1 | | 130 + 1 | 1 | [3, 30, 300] | 30 | null | | 130 + 1 | 1 | [3, 30, 300] | 30 | 2 | | 130 + 1 | 1 | [3, 30, 300] | 300 | 1 | | 400 + 1 | 1 | [3, 30, 300] | 300 | null | | 400 + 1 | 1 | [3, 30, 300] | 300 | 2 | | 400 + 2 | 2 | | | | | + 3 | | | | | | +(20 rows) + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + x | y | y | z +---+---+--------------+--- + 2 | 1 | [1, 2, 3] | 1 + 2 | 1 | [1, 2, 3] | 2 + 2 | 1 | [1, 2, 3] | 3 + 3 | 1 | [1, 2, 3] | 1 + 3 | 1 | [1, 2, 3] | 2 + 3 | 1 | [1, 2, 3] | 3 + 3 | 1 | [2, 3, 4, 5] | 2 + 3 | 1 | [2, 3, 4, 5] | 3 + 3 | 1 | [2, 3, 4, 5] | 4 + 3 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [1, 2, 3] | 1 + 4 | 1 | [1, 2, 3] | 2 + 4 | 1 | [1, 2, 3] | 3 + 4 | 1 | [2, 3, 4, 5] | 2 + 4 | 1 | [2, 3, 4, 5] | 3 + 4 | 1 | [2, 3, 4, 5] | 4 + 4 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [3, 4, 5, 6] | 3 + 4 | 1 | [3, 4, 5, 6] | 4 + 4 | 1 | [3, 4, 5, 6] | 5 + 4 | 1 | [3, 4, 5, 6] | 6 + 2 | 2 | [1, 2, 3] | 2 + 2 | 2 | [1, 2, 3] | 3 + 3 | 2 | [1, 2, 3] | 2 + 3 | 2 | [1, 2, 3] | 3 + 3 | 2 | [2, 3, 4, 5] | 2 + 3 | 2 | [2, 3, 4, 5] | 3 + 3 | 2 | [2, 3, 4, 5] | 4 + 3 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [1, 2, 3] | 2 + 4 | 2 | [1, 2, 3] | 3 + 4 | 2 | [2, 3, 4, 5] | 2 + 4 | 2 | [2, 3, 4, 5] | 3 + 4 | 2 | [2, 3, 4, 5] | 4 + 4 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [3, 4, 5, 6] | 3 + 4 | 2 | [3, 4, 5, 6] | 4 + 4 | 2 | [3, 4, 5, 6] | 5 + 4 | 2 | [3, 4, 5, 6] | 6 + 2 | 3 | [1, 2, 3] | 3 + 3 | 3 | [1, 2, 3] | 3 + 3 | 3 | [2, 3, 4, 5] | 3 + 3 | 3 | [2, 3, 4, 5] | 4 + 3 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [1, 2, 3] | 3 + 4 | 3 | [2, 3, 4, 5] | 3 + 4 | 3 | [2, 3, 4, 5] | 4 + 4 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [3, 4, 5, 6] | 3 + 4 | 3 | [3, 4, 5, 6] | 4 + 4 | 3 | [3, 4, 5, 6] | 5 + 4 | 3 | [3, 4, 5, 6] | 6 +(52 rows) + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; +ERROR: could not find jsonpath variable "x" +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); + json_value +------------ + foo +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_query +------------ + 123 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); + json_query +------------ + [123] +(1 row) + +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +ERROR: syntax error at or near " " of jsonpath input +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); +ERROR: only string constants supported in JSON_TABLE path specification +LINE 1: SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || '... + ^ diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql index 4f30fa46b9..df4a430d88 100644 --- a/src/test/regress/sql/json_sqljson.sql +++ b/src/test/regress/sql/json_sqljson.sql @@ -9,3 +9,7 @@ SELECT JSON_VALUE(NULL FORMAT JSON, '$'); -- JSON_QUERY SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + +-- JSON_TABLE + +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql index 963129699b..b8ef02803b 100644 --- a/src/test/regress/sql/jsonb_sqljson.sql +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -323,3 +323,632 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; + +-- JSON_TABLE + +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); + +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); + +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); + +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + +-- JSON_TABLE: Test backward parsing + +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); + +\sv jsonb_table_view + +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; + +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); + +-- JSON_TABLE: nested paths and plans + +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; + +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; + +-- JSON_TABLE: plan validation + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; + +-- JSON_TABLE: plan execution + +CREATE TEMP TABLE jsonb_table_test (js jsonb); + +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); + +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; + +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 336b7faa36..8c0cca7c51 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1303,6 +1303,7 @@ JsonPathKeyword JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback +JsonPathSpec JsonPathString JsonPathVarCallback JsonPathVariable @@ -1311,6 +1312,17 @@ JsonQuotes JsonReturning JsonSemAction JsonSerializeExpr +JsonTable +JsonTableColumn +JsonTableColumnType +JsonTableContext +JsonTableJoinState +JsonTableParent +JsonTablePlan +JsonTablePlanJoinType +JsonTablePlanType +JsonTableScanState +JsonTableSibling JsonTokenType JsonTransformStringValuesAction JsonTypeCategory @@ -2766,6 +2778,7 @@ TableFunc TableFuncRoutine TableFuncScan TableFuncScanState +TableFuncType TableInfo TableLikeClause TableSampleClause -- 2.35.3
v2-0004-SQL-JSON-query-functions.patch (application/octet-stream, 208.8K) download From 54464cbb1c588a364c8e61312ad5b848a4bc9fbe Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Fri, 16 Jun 2023 17:49:18 +0900 Subject: [PATCH v2 4/6] SQL/JSON query functions This introduces the SQL/JSON functions for querying JSON data using jsonpath expressions. The functions are: JSON_EXISTS() JSON_QUERY() JSON_VALUE() All of these functions only operate on jsonb. The workaround for now is to cast the argument to jsonb. JSON_EXISTS() tests if the jsonpath expression applied to the jsonb value yields any values. JSON_VALUE() must return a single value, and an error occurs if it tries to return multiple values. JSON_QUERY() must return a json object or array, and there are various WRAPPER options for handling scalar or multi-value results. Both these functions have options for handling EMPTY and ERROR conditions. Nikita Glukhov Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby. Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org --- doc/src/sgml/func.sgml | 147 +++ src/backend/executor/execExpr.c | 432 ++++++++ src/backend/executor/execExprInterp.c | 502 ++++++++- src/backend/jit/llvm/llvmjit_expr.c | 250 +++++ src/backend/jit/llvm/llvmjit_types.c | 5 + src/backend/nodes/makefuncs.c | 15 + src/backend/nodes/nodeFuncs.c | 183 ++++ src/backend/optimizer/path/costsize.c | 3 +- src/backend/optimizer/util/clauses.c | 19 + src/backend/parser/gram.y | 350 ++++++- src/backend/parser/parse_collate.c | 7 + src/backend/parser/parse_expr.c | 540 +++++++++- src/backend/parser/parse_target.c | 15 + src/backend/utils/adt/formatting.c | 44 + src/backend/utils/adt/jsonb.c | 62 ++ src/backend/utils/adt/jsonfuncs.c | 170 ++- src/backend/utils/adt/jsonpath.c | 255 +++++ src/backend/utils/adt/jsonpath_exec.c | 391 ++++++- src/backend/utils/adt/ruleutils.c | 136 +++ src/include/executor/execExpr.h | 172 +++ src/include/executor/executor.h | 1 + src/include/nodes/execnodes.h | 3 + src/include/nodes/makefuncs.h | 1 + src/include/nodes/parsenodes.h | 48 + src/include/nodes/primnodes.h | 109 ++ src/include/parser/kwlist.h | 11 + src/include/utils/formatting.h | 2 +- src/include/utils/jsonb.h | 3 + src/include/utils/jsonfuncs.h | 6 + src/include/utils/jsonpath.h | 27 + src/interfaces/ecpg/preproc/ecpg.trailer | 28 + src/test/regress/expected/json_sqljson.out | 18 + src/test/regress/expected/jsonb_sqljson.out | 1034 +++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/json_sqljson.sql | 11 + src/test/regress/sql/jsonb_sqljson.sql | 325 ++++++ src/tools/pgindent/typedefs.list | 14 + 37 files changed, 5239 insertions(+), 102 deletions(-) create mode 100644 src/test/regress/expected/json_sqljson.out create mode 100644 src/test/regress/expected/jsonb_sqljson.out create mode 100644 src/test/regress/sql/json_sqljson.sql create mode 100644 src/test/regress/sql/jsonb_sqljson.sql diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 8de8f527a5..e7a018583a 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16995,6 +16995,153 @@ array w/o UK? | t </tbody> </tgroup> </table> + + <para> + <xref linkend="functions-sqljson-querying"/> details the SQL/JSON + functions that can be used to query JSON data. + </para> + + <note> + <para> + SQL/JSON paths can only be applied to the <type>jsonb</type> type, so it + might be necessary to cast the <replaceable>context_item</replaceable> + argument of these functions to <type>jsonb</type>. + </para> + </note> + + <table id="functions-sqljson-querying"> + <title>SQL/JSON Query Functions</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="func_table_entry"><para role="func_signature"> + Function signature + </para> + <para> + Description + </para> + <para> + Example(s) + </para></entry> + </row> + </thead> + <tbody> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_exists</primary></indexterm> + <function>json_exists</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>TRUE</literal> | <literal>FALSE</literal> |<literal> UNKNOWN</literal> | <literal>ERROR</literal> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns true if the SQL/JSON <replaceable>path_expression</replaceable> + applied to the <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s yields any items. + The <literal>ON ERROR</literal> clause specifies what is returned if + an error occurs. Note that if the <replaceable>path_expression</replaceable> + is <literal>strict</literal>, an error is generated if it yields no items. + The default value is <literal>UNKNOWN</literal> which causes a NULL + result. + </para> + <para> + <literal>json_exists(jsonb '{"key1": [1,2,3]}', 'strict $.key1[*] ? (@ > 2)')</literal> + <returnvalue>t</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'lax $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>f</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'strict $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>ERROR: jsonpath array subscript is out of bounds</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_value</primary></indexterm> + <function>json_value</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> + <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. The extracted value must be + a single <acronym>SQL/JSON</acronym> scalar item. For results that + are objects or arrays, use the <function>json_query</function> + function instead. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + The <literal>ON EMPTY</literal> clause specifies the behavior if the + <replaceable>path_expression</replaceable> yields no value at all. + The <literal>ON ERROR</literal> clause specifies the behavior if an + error occurs as a result of <type>jsonpath</type> evaluation + (including cast to the output type) or execution of + <literal>ON EMPTY</literal> behavior (that was caused by empty result + of <type>jsonpath</type> evaluation). + </para> + <para> + <literal>json_value(jsonb '"123.45"', '$' RETURNING float)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_value(jsonb '"03:04 2015-02-01"', '$.datetime("HH24:MI&nbsp;YYYY-MM-DD")' RETURNING date)</literal> + <returnvalue>2015-02-01</returnvalue> + </para> + <para> + <literal>json_value(jsonb '[1,2]', 'strict $[*]' DEFAULT 9 ON ERROR)</literal> + <returnvalue>9</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_query</primary></indexterm> + <function>json_query</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional> + <optional> { <literal>WITHOUT</literal> | <literal>WITH</literal> { <literal>CONDITIONAL</literal> | <optional><literal>UNCONDITIONAL</literal></optional> } } <optional> <literal>ARRAY</literal> </optional> <literal>WRAPPER</literal> </optional> + <optional> { <literal>KEEP</literal> | <literal>OMIT</literal> } <literal>QUOTES</literal> <optional> <literal>ON SCALAR STRING</literal> </optional> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. + This function must return a JSON string, so if the path expression + returns multiple SQL/JSON items, you must wrap the result using the + <literal>WITH WRAPPER</literal> clause. If the wrapper is + <literal>UNCONDITIONAL</literal>, an array wrapper will always + be applied, even if the returned value is already a single JSON object + or array, but if it is <literal>CONDITIONAL</literal>, it will not be + applied to a single array or object. <literal>UNCONDITIONAL</literal> + is the default. + If the result is a scalar string, by default the value returned will have + surrounding quotes making it a valid JSON value. However, this behavior + is reversed if <literal>OMIT QUOTES</literal> is specified. + The <literal>ON ERROR</literal> and <literal>ON EMPTY</literal> + clauses have similar semantics to those clauses for + <function>json_value</function>. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + </para> + <para> + <literal>json_query(jsonb '[1,[2,3],null]', 'lax $[*][1]' WITH CONDITIONAL WRAPPER)</literal> + <returnvalue>[3]</returnvalue> + </para></entry> + </row> + </tbody> + </tgroup> + </table> + </sect2> <sect2 id="functions-sqljson-path"> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 95d9fbbc7c..dbf2e692ee 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -50,6 +50,7 @@ #include "utils/datum.h" #include "utils/json.h" #include "utils/jsonb.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -89,6 +90,16 @@ static void ExecBuildAggTransCall(ExprState *state, AggState *aggstate, FunctionCallInfo fcinfo, AggStatePerTrans pertrans, int transno, int setno, int setoff, bool ishash, bool nullcheck); +static void ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch); +static Datum GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null); +static JsonCoercionState *ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull); +static JsonItemCoercionsState *ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull); /* @@ -2428,6 +2439,14 @@ ExecInitExprRec(Expr *node, ExprState *state, break; } + case T_JsonExpr: + { + JsonExpr *jexpr = castNode(JsonExpr, node); + + ExecInitJsonExpr(jexpr, state, resv, resnull, &scratch); + break; + } + case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -4195,3 +4214,416 @@ ExecBuildParamSetEqual(TupleDesc desc, return state; } + +/* + * Push steps to evaluate a JsonExpr and its various subsidiary expressions. + */ +static void +ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch) +{ + JsonExprState *jsestate = palloc0(sizeof(JsonExprState)); + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + ListCell *argexprlc; + ListCell *argnamelc; + int skip_step_off = -1; + int passing_args_step_off = -1; + int coercion_step_off = -1; + int coercion_finish_step_off = -1; + int behavior_step_off = -1; + int onempty_expr_step_off = -1; + int onempty_jump_step_off = -1; + int onerror_expr_step_off = -1; + int onerror_jump_step_off = -1; + List *adjust_jumps = NIL; + ListCell *lc; + ExprEvalStep *as; + + jsestate->jsexpr = jexpr; + + /* + * Add steps to compute formatted_expr, pathspec, and PASSING arg + * expressions as things that must be evaluated *before* the actual JSON + * path expression. + */ + ExecInitExprRec((Expr *) jexpr->formatted_expr, state, + &pre_eval->formatted_expr.value, + &pre_eval->formatted_expr.isnull); + ExecInitExprRec((Expr *) jexpr->path_spec, state, + &pre_eval->pathspec.value, + &pre_eval->pathspec.isnull); + + /* + * Before pushing steps for PASSING args, push a step to decide whether to + * skip evaluating the args and the JSON path expression depending on + * whether either of formatted_expr and pathspec is NULL; see + * ExecEvalJsonExprSkip(). + */ + scratch->opcode = EEOP_JSONEXPR_SKIP; + scratch->d.jsonexpr_skip.jsestate = jsestate; + skip_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* PASSING args. */ + jsestate->pre_eval.args = NIL; + passing_args_step_off = state->steps_len; + forboth(argexprlc, jexpr->passing_values, + argnamelc, jexpr->passing_names) + { + Expr *argexpr = (Expr *) lfirst(argexprlc); + String *argname = lfirst_node(String, argnamelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(argname->sval); + var->typid = exprType((Node *) argexpr); + var->typmod = exprTypmod((Node *) argexpr); + + ExecInitExprRec((Expr *) argexpr, state, &var->value, &var->isnull); + + pre_eval->args = lappend(pre_eval->args, var); + } + + /* + * Step for the actual JSON path evaluation; see ExecEvalJson() and + * ExecEvalJsonExpr(). + */ + scratch->opcode = EEOP_JSONEXPR_PATH; + scratch->d.jsonexpr.jsestate = jsestate; + ExprEvalPushStep(state, scratch); + + /* + * Push steps to control the evaluation of expressions based on the result + * of JSON path evaluation. + */ + + /* + * Step to handle ON ERROR and ON EMPTY behavior; see + * ExecEvalJsonExprBehavior(). + */ + scratch->opcode = EEOP_JSONEXPR_BEHAVIOR; + scratch->d.jsonexpr_behavior.jsestate = jsestate; + behavior_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Step(s) to evaluate ON EMPTY expression */ + onempty_expr_step_off = state->steps_len; + if (jexpr->on_empty && + jexpr->on_empty->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_empty->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_empty->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_empty, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onempty_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + } + } + + /* Step(s) to evaluate ON ERROR expression */ + onerror_expr_step_off = state->steps_len; + if (jexpr->on_error && + jexpr->on_error->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_error->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_error->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_error, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onerror_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set later */ + ExprEvalPushStep(state, scratch); + } + } + + /* + * Step to handle applying coercion to the JSON item returned by + * EEOP_JSONEXPR_PATH or to the ON EMPTY/ERROR expression as + * EEOP_JSONEXPR_BEHAVIOR decides; see ExecEvalJsonExprCoercion(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Initialize coercion expression(s). */ + if (jexpr->result_coercion) + { + jsestate->result_jcstate = + ExecInitJsonCoercion(scratch, state, jexpr->result_coercion, + resv, resnull); + /* See the comment above. */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* computed later */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + if (jexpr->coercions) + { + /* + * ExecPrepareJsonItemCoercion() called by ExecEvalJsonExpr() chooses + * one for a given JSON item returned by JsonPathValue(). + */ + jsestate->item_jcstates = + ExecInitJsonItemCoercions(scratch, state, jexpr->coercions, + resv, resnull); + } + + /* + * And a step to clean up after the coercion step; see + * ExecEvalJsonExprCoercionFinish(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION_FINISH; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_finish_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* + * Adjust jump target addresses in various post-eval steps now that we + * have all the steps in place. + */ + + /* EEOP_JSONEXPR_SKIP */ + Assert(skip_step_off >= 0); + as = &state->steps[skip_step_off]; + as->d.jsonexpr_skip.jump_coercion = coercion_step_off; + as->d.jsonexpr_skip.jump_passing_args = passing_args_step_off; + + /* EEOP_JSONEXPR_BEHAVIOR */ + Assert(behavior_step_off >= 0); + as = &state->steps[behavior_step_off]; + as->d.jsonexpr_behavior.jump_onerror_expr = onerror_expr_step_off; + as->d.jsonexpr_behavior.jump_onempty_expr = onempty_expr_step_off; + as->d.jsonexpr_behavior.jump_coercion = coercion_step_off; + as->d.jsonexpr_behavior.jump_skip_coercion = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION */ + Assert(coercion_step_off >= 0); + as = &state->steps[coercion_step_off]; + as->d.jsonexpr_coercion.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION_FINISH */ + Assert(coercion_finish_step_off >= 0); + as = &state->steps[coercion_finish_step_off]; + as->d.jsonexpr_coercion_finish.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion_finish.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JUMP steps */ + + /* + * Ones after ON EMPTY and ON ERROR non-default expressions should jump to + * the coercion step. + */ + if (onempty_jump_step_off >= 0) + { + as = &state->steps[onempty_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + if (onerror_jump_step_off >= 0) + { + as = &state->steps[onerror_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + + /* The rest should jump to the end. */ + foreach(lc, adjust_jumps) + { + as = &state->steps[lfirst_int(lc)]; + as->d.jump.jumpdone = state->steps_len; + } + + /* + * Set RETURNING type's input function used by ExecEvalJsonExprCoercion(). + */ + if (jexpr->omit_quotes || + (jexpr->result_coercion && jexpr->result_coercion->via_io)) + { + Oid typinput; + FmgrInfo *finfo; + + /* lookup the result type's input function */ + getTypeInputInfo(jexpr->returning->typid, &typinput, + &jsestate->input.typioparam); + finfo = palloc0(sizeof(FmgrInfo)); + fmgr_info(typinput, finfo); + jsestate->input.finfo = finfo; + } +} + +/* + * Evaluate a JSON error/empty behavior result. + */ +static Datum +GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null) +{ + *is_null = false; + + switch (behavior->btype) + { + case JSON_BEHAVIOR_EMPTY_ARRAY: + return JsonbPGetDatum(JsonbMakeEmptyArray()); + + case JSON_BEHAVIOR_EMPTY_OBJECT: + return JsonbPGetDatum(JsonbMakeEmptyObject()); + + case JSON_BEHAVIOR_TRUE: + return BoolGetDatum(true); + + case JSON_BEHAVIOR_FALSE: + return BoolGetDatum(false); + + case JSON_BEHAVIOR_NULL: + case JSON_BEHAVIOR_UNKNOWN: + case JSON_BEHAVIOR_EMPTY: + *is_null = true; + return (Datum) 0; + + case JSON_BEHAVIOR_DEFAULT: + /* Always handled in the caller. */ + Assert(false); + return (Datum) 0; + + default: + elog(ERROR, "unrecognized SQL/JSON behavior %d", behavior->btype); + return (Datum) 0; + } +} + +/* Initialize one JsonCoercion for execution. */ +static JsonCoercionState * +ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull) +{ + JsonCoercionState *jcstate = palloc0(sizeof(JsonCoercionState)); + + jcstate->coercion = coercion; + if (coercion && coercion->expr) + { + Datum *save_innermost_caseval; + bool *save_innermost_casenull; + + jcstate->jump_eval_expr = state->steps_len; + + /* Push step(s) to compute cstate->coercion. */ + save_innermost_caseval = state->innermost_caseval; + save_innermost_casenull = state->innermost_casenull; + + state->innermost_caseval = resv; + state->innermost_casenull = resnull; + + ExecInitExprRec((Expr *) coercion->expr, state, resv, resnull); + + state->innermost_caseval = save_innermost_caseval; + state->innermost_casenull = save_innermost_casenull; + } + else + jcstate->jump_eval_expr = -1; + + return jcstate; +} + +/* + * Push steps to evaluate coercions from a given JsonItemCoercions, which + * contains all possible coercions that may need to be applied to JSON + * items coming from evaluating the parent JsonExpr. + */ +static JsonItemCoercionsState * +ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull) +{ + JsonCoercion **coercion; + JsonItemCoercionsState *item_jcstates = palloc0(sizeof(JsonItemCoercionsState)); + JsonCoercionState **item_jcstate; + ExprEvalStep *as; + List *adjust_jumps = NIL; + ListCell *lc; + + /* Push the steps of individual coercions. */ + for (coercion = &coercions->null, + item_jcstate = &item_jcstates->null; + coercion <= &coercions->composite; + coercion++, item_jcstate++) + { + *item_jcstate = ExecInitJsonCoercion(scratch, state, *coercion, + resv, resnull); + + /* Emit JUMP step to skip past other coercions' steps. */ + scratch->opcode = EEOP_JUMP; + + /* + * Remember JUMP step address to set the actual jump target address + * below. + */ + adjust_jumps = lappend_int(adjust_jumps, state->steps_len); + ExprEvalPushStep(state, scratch); + } + + foreach(lc, adjust_jumps) + { + int jump_step_id = lfirst_int(lc); + + as = &state->steps[jump_step_id]; + as->d.jump.jumpdone = state->steps_len; + } + + return item_jcstates; +} diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index b83dd1c666..4b0647153a 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -74,6 +74,7 @@ #include "utils/json.h" #include "utils/jsonb.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/timestamp.h" @@ -152,6 +153,9 @@ static TupleDesc get_cached_rowtype(Oid type_id, int32 typmod, bool *changed); static void ExecEvalRowNullInt(ExprState *state, ExprEvalStep *op, ExprContext *econtext, bool checkisnull); +static Datum ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate); /* fast-path evaluation functions */ static Datum ExecJustInnerVar(ExprState *state, ExprContext *econtext, bool *isnull); @@ -480,6 +484,11 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_XMLEXPR, &&CASE_EEOP_JSON_CONSTRUCTOR, &&CASE_EEOP_IS_JSON, + &&CASE_EEOP_JSONEXPR_SKIP, + &&CASE_EEOP_JSONEXPR_PATH, + &&CASE_EEOP_JSONEXPR_BEHAVIOR, + &&CASE_EEOP_JSONEXPR_COERCION, + &&CASE_EEOP_JSONEXPR_COERCION_FINISH, &&CASE_EEOP_AGGREF, &&CASE_EEOP_GROUPING_FUNC, &&CASE_EEOP_WINDOW_FUNC, @@ -1186,8 +1195,14 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) /* second and third arguments are already set up */ fcinfo_in->isnull = false; + + /* Pass down soft-error capture node if any. */ + fcinfo_in->context = state->escontext; + d = FunctionCallInvoke(fcinfo_in); *op->resvalue = d; + if (SOFT_ERROR_OCCURRED(state->escontext)) + *op->resnull = true; /* Should get null result if and only if str is NULL */ if (str == NULL) @@ -1195,7 +1210,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) Assert(*op->resnull); Assert(fcinfo_in->isnull); } - else + else if (!SOFT_ERROR_OCCURRED(state->escontext)) { Assert(!*op->resnull); Assert(!fcinfo_in->isnull); @@ -1543,6 +1558,38 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_JSONEXPR_PATH) + { + /* too complex for an inline implementation */ + ExecEvalJsonExpr(state, op, econtext); + EEO_NEXT(); + } + + EEO_CASE(EEOP_JSONEXPR_SKIP) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprSkip(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_BEHAVIOR) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprBehavior(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercion(state, op, econtext, + *op->resvalue, *op->resnull)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION_FINISH) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercionFinish(state, op)); + } + EEO_CASE(EEOP_AGGREF) { /* @@ -3745,7 +3792,7 @@ ExecEvalConstraintCheck(ExprState *state, ExprEvalStep *op) { if (!*op->d.domaincheck.checknull && !DatumGetBool(*op->d.domaincheck.checkvalue)) - ereport(ERROR, + errsave(state->escontext, (errcode(ERRCODE_CHECK_VIOLATION), errmsg("value for domain %s violates check constraint \"%s\"", format_type_be(op->d.domaincheck.resulttype), @@ -4137,6 +4184,457 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op) *op->resvalue = BoolGetDatum(res); } +/* + * Evaluate given JsonExpr by performing the specified JSON operation. + * + * This also populates the JsonExprPostEvalState with the information needed + * by the subsequent steps that handle the specified JsonBehavior. + */ +void +ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) +{ + JsonExprState *jsestate = op->d.jsonexpr.jsestate; + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonExpr *jexpr = jsestate->jsexpr; + Datum item; + Datum res = (Datum) 0; + bool resnull = true; + JsonPath *path; + bool *error; + bool *empty; + + item = pre_eval->formatted_expr.value; + path = DatumGetJsonPathP(pre_eval->pathspec.value); + + /* Reset JsonExprPostEvalState for this evaluation. */ + memset(post_eval, 0, sizeof(*post_eval)); + + /* Respect ON ERROR behavior during path evaluation. */ + jsestate->throw_error = (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR); + + /* + * Be sure to save the error (if needed) and empty statuses for the + * EEOP_JSONEXPR_BEHAVIOR step to peruse. + */ + error = !jsestate->throw_error ? &post_eval->error : NULL; + empty = &post_eval->empty; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + res = JsonPathQuery(item, path, jexpr->wrapper, empty, error, + pre_eval->args); + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + resnull = !DatumGetPointer(res); + break; + + case JSON_VALUE_OP: + { + JsonbValue *jbv = JsonPathValue(item, path, empty, error, + pre_eval->args); + + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + if (!jbv) /* NULL or empty */ + { + resnull = true; + break; + } + + Assert(!*empty); + + resnull = false; + + /* coerce scalar item to the output type */ + if (jexpr->returning->typid == JSONOID || + jexpr->returning->typid == JSONBOID) + { + /* Use result coercion from json[b] to the output type */ + res = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + break; + } + + /* + * Error out if no cast exists to coerce SQL/JSON item to the + * the output type. + */ + Assert(post_eval->item_jcstate == NULL); + res = ExecPrepareJsonItemCoercion(jbv, + jsestate->item_jcstates, + &post_eval->item_jcstate); + if (post_eval->item_jcstate && + post_eval->item_jcstate->coercion && + (post_eval->item_jcstate->coercion->via_io || + post_eval->item_jcstate->coercion->via_populate)) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * Coercion via I/O means here that the cast to the target + * type simply does not exist. + */ + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE), + errmsg("SQL/JSON item cannot be cast to target type"))); + } + break; + } + + case JSON_EXISTS_OP: + { + bool exists = JsonPathExists(item, path, + pre_eval->args, + error); + + resnull = error && *error; + res = BoolGetDatum(exists); + break; + } + + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * If the ON EMPTY behavior is to cause an error, do so here. Other + * behaviors will be handled by the caller. + */ + if (*empty) + { + Assert(jexpr->on_empty); /* it is not JSON_EXISTS */ + + if (jexpr->on_empty->btype == JSON_BEHAVIOR_ERROR) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + ereport(ERROR, + (errcode(ERRCODE_NO_SQL_JSON_ITEM), + errmsg("no SQL/JSON item"))); + } + } + + *op->resvalue = res; + *op->resnull = resnull; +} + +/* + * Skip calling ExecEvalJson() on the given JsonExpr? + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_skip.jsestate; + + /* + * Skip if either of the input expressions has turned out to be NULL, + * though do execute domain checks for NULLs, which are handled by the + * coercion step. + */ + if (jsestate->pre_eval.formatted_expr.isnull || + jsestate->pre_eval.pathspec.isnull) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_skip.jump_coercion; + } + + /* + * Go evaluate the PASSING args if any and subsequently JSON path itself. + */ + return op->d.jsonexpr_skip.jump_passing_args; +} + +/* + * Returns the step address to perform the JsonBehavior applicable to + * the JSON item that resulted from evaluating the given JsonExpr. + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_behavior.jsestate; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonBehavior *behavior = NULL; + int jump_to = -1; + + if (post_eval->error || post_eval->coercion_error) + { + behavior = jsestate->jsexpr->on_error; + jump_to = op->d.jsonexpr_behavior.jump_onerror_expr; + } + else if (post_eval->empty) + { + behavior = jsestate->jsexpr->on_empty; + jump_to = op->d.jsonexpr_behavior.jump_onempty_expr; + } + else if (!post_eval->coercion_done) + { + /* + * If no error or the JSON item is not empty, directly go to the + * coercion step to coerce the item as is. + */ + return op->d.jsonexpr_behavior.jump_coercion; + } + + Assert(behavior); + + /* + * Set up for coercion step that will run to coerce a non-default behavior + * value. It should use result_coercion, if any. Errors that may occur + * should be thrown for JSON ops other than JSON_VALUE_OP. + */ + if (behavior->btype != JSON_BEHAVIOR_DEFAULT) + { + post_eval->item_jcstate = NULL; + jsestate->throw_error = (jsestate->jsexpr->op != JSON_VALUE_OP); + } + + Assert(jump_to >= 0); + return jump_to; +} + +/* + * Evaluate or return the step address to evaluate a coercion of a JSON item + * to the target type. The former if the coercion must be done right away by + * calling its input function or by calling json_populate_type(). + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull) +{ + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonExpr *jexpr = jsestate->jsexpr; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonCoercionState *item_jcstate = post_eval->item_jcstate; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + + if (item_jcstate == NULL && + jsestate->jsexpr->op != JSON_EXISTS_OP) + { + ErrorSaveContext escontext = {T_ErrorSaveContext}; + Node *escontext_p; + JsonCoercion *coercion; + Jsonb *jb; + + escontext_p = !jsestate->throw_error ? (Node *) &escontext : NULL; + coercion = result_jcstate ? result_jcstate->coercion : NULL; + jb = resnull ? NULL : DatumGetJsonbP(res); + + if ((coercion && coercion->via_io) || + (jexpr->omit_quotes && !resnull && + JB_ROOT_IS_SCALAR(jb))) + { + /* strip quotes and call typinput function */ + char *str = resnull ? NULL : JsonbUnquote(jb); + bool type_is_domain; + + type_is_domain = (getBaseType(jexpr->returning->typid) != + jexpr->returning->typid); + + /* + * Catch errors only if the type is not a domain, because errors + * caused by a domain's constraint failure must be thrown right + * away. + */ + if (!InputFunctionCallSafe(jsestate->input.finfo, str, + jsestate->input.typioparam, + jexpr->returning->typmod, + !type_is_domain ? escontext_p : NULL, + op->resvalue)) + { + post_eval->error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + else if (coercion && coercion->via_populate) + { + *op->resvalue = json_populate_type(res, JSONBOID, + jexpr->returning->typid, + jexpr->returning->typmod, + &post_eval->cache, + econtext->ecxt_per_query_memory, + op->resnull, + escontext_p); + if (SOFT_ERROR_OCCURRED(escontext_p)) + { + post_eval->error = true; + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + } + + if (!jsestate->throw_error) + { + post_eval->escontext.type = T_ErrorSaveContext; + state->escontext = (Node *) &post_eval->escontext; + } + + if (item_jcstate && item_jcstate->jump_eval_expr >= 0) + return item_jcstate->jump_eval_expr; + else if (result_jcstate && result_jcstate->jump_eval_expr >= 0) + return result_jcstate->jump_eval_expr; + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; +} + +/* + * Checks if the coercion evaluation led to an error. If an error did occur, + * this returns the address of the step that handles the error, otherwise + * the step after the coercion step, which finishes the JsonExpr processing. + */ +int +ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op) +{ + JsonExprPostEvalState *post_eval = + &op->d.jsonexpr_coercion_finish.jsestate->post_eval; + + if (SOFT_ERROR_OCCURRED(state->escontext)) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + post_eval->coercion_error = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_error; + } + + /* Reset. */ + state->escontext = NULL; + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_done; +} + +/* + * Prepare SQL/JSON item coercion to the output type. Returned a datum of the + * corresponding SQL type and a pointer to the coercion state. + */ +static Datum +ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate) +{ + JsonCoercionState *item_jcstate; + Datum res; + JsonbValue buf; + + if (item->type == jbvBinary && + JsonContainerIsScalar(item->val.binary.data)) + { + bool res PG_USED_FOR_ASSERTS_ONLY; + + res = JsonbExtractScalar(item->val.binary.data, &buf); + item = &buf; + Assert(res); + } + + /* get coercion state reference and datum of the corresponding SQL type */ + switch (item->type) + { + case jbvNull: + item_jcstate = item_jcstates->null; + res = (Datum) 0; + break; + + case jbvString: + item_jcstate = item_jcstates->string; + res = PointerGetDatum(cstring_to_text_with_len(item->val.string.val, + item->val.string.len)); + break; + + case jbvNumeric: + item_jcstate = item_jcstates->numeric; + res = NumericGetDatum(item->val.numeric); + break; + + case jbvBool: + item_jcstate = item_jcstates->boolean; + res = BoolGetDatum(item->val.boolean); + break; + + case jbvDatetime: + res = item->val.datetime.value; + switch (item->val.datetime.typid) + { + case DATEOID: + item_jcstate = item_jcstates->date; + break; + case TIMEOID: + item_jcstate = item_jcstates->time; + break; + case TIMETZOID: + item_jcstate = item_jcstates->timetz; + break; + case TIMESTAMPOID: + item_jcstate = item_jcstates->timestamp; + break; + case TIMESTAMPTZOID: + item_jcstate = item_jcstates->timestamptz; + break; + default: + elog(ERROR, "unexpected jsonb datetime type oid %u", + item->val.datetime.typid); + return (Datum) 0; + } + break; + + case jbvArray: + case jbvObject: + case jbvBinary: + item_jcstate = item_jcstates->composite; + res = JsonbPGetDatum(JsonbValueToJsonb(item)); + break; + + default: + elog(ERROR, "unexpected jsonb value type %d", item->type); + return (Datum) 0; + } + + *p_item_jcstate = item_jcstate; + + return res; +} + /* * ExecEvalGroupingFunc diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 00d7b8110b..da3870cba6 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -1860,6 +1860,256 @@ llvm_compile_expr(ExprState *state) LLVMBuildBr(b, opblocks[opno + 1]); break; + case EEOP_JSONEXPR_PATH: + build_EvalXFunc(b, mod, "ExecEvalJsonExpr", + v_state, op, v_econtext); + LLVMBuildBr(b, opblocks[opno + 1]); + break; + + case EEOP_JSONEXPR_SKIP: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprSkip() to decide if JSON path + * evaluation can be skipped. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprSkip"), + params, lengthof(params), ""); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_skip.jump_coercion, which signifies + * skipping of JSON path evaluation, else to the next step + * which must point to the steps to evaluate PASSING args, + * if any, or to the JSON path evaluation. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_skip.jump_coercion), + ""), + opblocks[op->d.jsonexpr_skip.jump_coercion], + opblocks[opno + 1]); + break; + } + + case EEOP_JSONEXPR_BEHAVIOR: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + LLVMBasicBlockRef b_jump_onerror_default; + + /* + * Call ExecEvalJsonExprBehavior() to decide if ON EMPTY + * or ON ERROR behavior must be invoked depending on what + * JSON path evaluation returned. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprBehavior"), + params, lengthof(params), ""); + + b_jump_onerror_default = + l_bb_before_v(opblocks[opno + 1], + "op.%d.jsonexpr_behavior_jump_onerror_default", opno); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_behavior.jump_coercion, else to the + * next block, one that checks whether to evaluate the ON + * ERROR default expression. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_coercion), + ""), + opblocks[op->d.jsonexpr_behavior.jump_coercion], + b_jump_onerror_default); + + /* + * Block that checks whether to evaluate the ON ERROR + * default expression. + * + * Jump to evaluate the ON ERROR default expression if the + * returned address is the same as + * jsonexpr_behavior.jump_onerror_default, else jump to + * evaluate the ON EMPTY default expression. + */ + LLVMPositionBuilderAtEnd(b, b_jump_onerror_default); + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_onerror_expr), + ""), + opblocks[op->d.jsonexpr_behavior.jump_onerror_expr], + opblocks[op->d.jsonexpr_behavior.jump_onempty_expr]); + break; + } + case EEOP_JSONEXPR_COERCION: + { + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonItemCoercionsState *item_jcstates = jsestate->item_jcstates; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + LLVMValueRef v_ret; + LLVMValueRef params[5]; + + /* + * Call ExecEvalJsonExprCoercion() to evaluate appropriate + * coercion. This will return the step address to jump + * to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + params[2] = v_econtext; + params[3] = v_resvaluep; + params[4] = v_resnullp; + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercion"), + params, lengthof(params), ""); + + /* + * Jump to handle a coercion error if the returned address + * is the same as jsonexpr_coercion.jump_coercion_error, + * else to the step after coercion (coercion done!). + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + + if (item_jcstates) + { + JsonCoercionState **item_jcstate; + int n_coercions = (int) + (item_jcstates->composite - item_jcstates->null) + 1; + int i; + LLVMBasicBlockRef *b_coercions; + + + /* + * Will create a block for each coercion below to + * check whether to evaluate the coercion's expression + * if there's one or to skip to the end if not. + */ + b_coercions = palloc((n_coercions + 1) * sizeof(LLVMBasicBlockRef)); + for (i = 0; i < n_coercions + 1; i++) + b_coercions[i] = + l_bb_before_v(opblocks[opno + 1], + "op.%d.json_item_coercion.%d", + opno, i); + + /* Jump to check first coercion */ + LLVMBuildBr(b, b_coercions[0]); + + /* + * Add conditional branches for individual coercion's + * expressions + */ + for (item_jcstate = &item_jcstates->null, i = 0; + item_jcstate <= &item_jcstates->composite; + item_jcstate++, i++) + { + /* Block for this coercion */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + + /* + * Jump to evaluate the coercion's expression if + * the address returned is the same as this + * coercion's jump_eval_expr (that is, if it is + * valid), else check the next coercion's. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const((*item_jcstate)->jump_eval_expr), + ""), + (*item_jcstate)->jump_eval_expr >= 0 ? + opblocks[(*item_jcstate)->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + b_coercions[i + 1]); + } + + /* + * A placeholder block that the last coercion's block + * might jump to, which unconditionally jumps to end + * of coercions. + */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + LLVMBuildBr(b, opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + + /* + * Jump to evaluate the result_coercion's expression if + * none of the above coercions matched, that is, if + * there's one. + */ + if (result_jcstate) + { + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(result_jcstate->jump_eval_expr), + ""), + result_jcstate->jump_eval_expr >= 0 ? + opblocks[result_jcstate->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + break; + } + + case EEOP_JSONEXPR_COERCION_FINISH: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprCoercionFinish() to check whether + * an coercion error occurred, in which case we must jump + * to whatever step handles the error. This returns the + * step address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercionFinish"), + params, lengthof(params), ""); + + /* + * Jump to the step that handles coercion error if the + * returned address is the same as + * jsonexpr_coercion_finish.jump_coercion_error, else to + * jsonexpr_coercion_finish.jump_coercion_done. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion_finish.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_done]); + break; + } + case EEOP_AGGREF: { LLVMValueRef v_aggno; diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c index 41ac4c6f45..cf3ced3427 100644 --- a/src/backend/jit/llvm/llvmjit_types.c +++ b/src/backend/jit/llvm/llvmjit_types.c @@ -135,6 +135,11 @@ void *referenced_functions[] = ExecEvalXmlExpr, ExecEvalJsonConstructor, ExecEvalJsonIsPredicate, + ExecEvalJsonExprSkip, + ExecEvalJsonExprBehavior, + ExecEvalJsonExprCoercion, + ExecEvalJsonExprCoercionFinish, + ExecEvalJsonExpr, MakeExpandedObjectReadOnlyInternal, slot_getmissingattrs, slot_getsomeattrs_int, diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 39e1884cf4..5dc3aa2e9f 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -859,6 +859,21 @@ makeJsonValueExpr(Expr *expr, JsonFormat *format) return jve; } +/* + * makeJsonBehavior - + * creates a JsonBehavior node + */ +JsonBehavior * +makeJsonBehavior(JsonBehaviorType type, Node *default_expr) +{ + JsonBehavior *behavior = makeNode(JsonBehavior); + + behavior->btype = type; + behavior->default_expr = default_expr; + + return behavior; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index dda964bd19..d31371bb4d 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -236,6 +236,12 @@ exprType(const Node *expr) case T_JsonIsPredicate: type = BOOLOID; break; + case T_JsonExpr: + type = ((const JsonExpr *) expr)->returning->typid; + break; + case T_JsonCoercion: + type = exprType(((const JsonCoercion *) expr)->expr); + break; case T_NullTest: type = BOOLOID; break; @@ -495,6 +501,10 @@ exprTypmod(const Node *expr) return exprTypmod((Node *) ((const JsonValueExpr *) expr)->formatted_expr); case T_JsonConstructorExpr: return ((const JsonConstructorExpr *) expr)->returning->typmod; + case T_JsonExpr: + return ((JsonExpr *) expr)->returning->typmod; + case T_JsonCoercion: + return exprTypmod(((const JsonCoercion *) expr)->expr); case T_CoerceToDomain: return ((const CoerceToDomain *) expr)->resulttypmod; case T_CoerceToDomainValue: @@ -971,6 +981,22 @@ exprCollation(const Node *expr) /* IS JSON's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + coll = InvalidOid; + else if (coercion->expr) + coll = exprCollation(coercion->expr); + else if (coercion->via_io || coercion->via_populate) + coll = coercion->collation; + else + coll = InvalidOid; + } + break; + case T_NullTest: /* NullTest's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ @@ -1207,6 +1233,21 @@ exprSetCollation(Node *expr, Oid collation) case T_JsonIsPredicate: Assert(!OidIsValid(collation)); /* result is always boolean */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + Assert(!OidIsValid(collation)); + else if (coercion->expr) + exprSetCollation(coercion->expr, collation); + else if (coercion->via_io || coercion->via_populate) + coercion->collation = collation; + else + Assert(!OidIsValid(collation)); + } + break; case T_NullTest: /* NullTest's result is boolean ... */ Assert(!OidIsValid(collation)); /* ... so never set a collation */ @@ -1510,6 +1551,15 @@ exprLocation(const Node *expr) case T_JsonIsPredicate: loc = ((const JsonIsPredicate *) expr)->location; break; + case T_JsonExpr: + { + const JsonExpr *jsexpr = (const JsonExpr *) expr; + + /* consider both function name and leftmost arg */ + loc = leftmostLoc(jsexpr->location, + exprLocation(jsexpr->formatted_expr)); + } + break; case T_NullTest: { const NullTest *nexpr = (const NullTest *) expr; @@ -2262,6 +2312,54 @@ expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + if (WALK(jexpr->formatted_expr)) + return true; + if (WALK(jexpr->result_coercion)) + return true; + if (WALK(jexpr->passing_values)) + return true; + /* we assume walker doesn't care about passing_names */ + if (jexpr->on_empty && + WALK(jexpr->on_empty->default_expr)) + return true; + if (WALK(jexpr->on_error->default_expr)) + return true; + if (WALK(jexpr->coercions)) + return true; + } + break; + case T_JsonCoercion: + return WALK(((JsonCoercion *) node)->expr); + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + + if (WALK(coercions->null)) + return true; + if (WALK(coercions->string)) + return true; + if (WALK(coercions->numeric)) + return true; + if (WALK(coercions->boolean)) + return true; + if (WALK(coercions->date)) + return true; + if (WALK(coercions->time)) + return true; + if (WALK(coercions->timetz)) + return true; + if (WALK(coercions->timestamp)) + return true; + if (WALK(coercions->timestamptz)) + return true; + if (WALK(coercions->composite)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: @@ -3261,6 +3359,54 @@ expression_tree_mutator_impl(Node *node, return (Node *) newnode; } + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + JsonExpr *newnode; + + FLATCOPY(newnode, jexpr, JsonExpr); + MUTATE(newnode->path_spec, jexpr->path_spec, Node *); + MUTATE(newnode->formatted_expr, jexpr->formatted_expr, Node *); + MUTATE(newnode->result_coercion, jexpr->result_coercion, JsonCoercion *); + MUTATE(newnode->passing_values, jexpr->passing_values, List *); + /* assume mutator does not care about passing_names */ + if (newnode->on_empty) + MUTATE(newnode->on_empty->default_expr, + jexpr->on_empty->default_expr, Node *); + MUTATE(newnode->on_error->default_expr, + jexpr->on_error->default_expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonCoercion: + { + JsonCoercion *coercion = (JsonCoercion *) node; + JsonCoercion *newnode; + + FLATCOPY(newnode, coercion, JsonCoercion); + MUTATE(newnode->expr, coercion->expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + JsonItemCoercions *newnode; + + FLATCOPY(newnode, coercions, JsonItemCoercions); + MUTATE(newnode->null, coercions->null, JsonCoercion *); + MUTATE(newnode->string, coercions->string, JsonCoercion *); + MUTATE(newnode->numeric, coercions->numeric, JsonCoercion *); + MUTATE(newnode->boolean, coercions->boolean, JsonCoercion *); + MUTATE(newnode->date, coercions->date, JsonCoercion *); + MUTATE(newnode->time, coercions->time, JsonCoercion *); + MUTATE(newnode->timetz, coercions->timetz, JsonCoercion *); + MUTATE(newnode->timestamp, coercions->timestamp, JsonCoercion *); + MUTATE(newnode->timestamptz, coercions->timestamptz, JsonCoercion *); + MUTATE(newnode->composite, coercions->composite, JsonCoercion *); + return (Node *) newnode; + } + break; case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -3947,6 +4093,43 @@ raw_expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonArgument: + return WALK(((JsonArgument *) node)->val); + case T_JsonCommon: + { + JsonCommon *jc = (JsonCommon *) node; + + if (WALK(jc->expr)) + return true; + if (WALK(jc->pathspec)) + return true; + if (WALK(jc->passing)) + return true; + } + break; + case T_JsonBehavior: + { + JsonBehavior *jb = (JsonBehavior *) node; + + if (jb->btype == JSON_BEHAVIOR_DEFAULT && + WALK(jb->default_expr)) + return true; + } + break; + case T_JsonFuncExpr: + { + JsonFuncExpr *jfe = (JsonFuncExpr *) node; + + if (WALK(jfe->common)) + return true; + if (jfe->output && WALK(jfe->output)) + return true; + if (WALK(jfe->on_empty)) + return true; + if (WALK(jfe->on_error)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index ef475d95a1..f58c275b4b 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -4609,7 +4609,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context) IsA(node, SQLValueFunction) || IsA(node, XmlExpr) || IsA(node, CoerceToDomain) || - IsA(node, NextValueExpr)) + IsA(node, NextValueExpr) || + IsA(node, JsonExpr)) { /* Treat all these as having cost 1 */ context->total.per_tuple += cpu_operator_cost; diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 59d8ce789e..2dacb83052 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -53,6 +53,7 @@ #include "utils/fmgroids.h" #include "utils/json.h" #include "utils/jsonb.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/syscache.h" @@ -412,6 +413,24 @@ contain_mutable_functions_walker(Node *node, void *context) /* Check all subnodes */ } + if (IsA(node, JsonExpr)) + { + JsonExpr *jexpr = castNode(JsonExpr, node); + Const *cnst; + + if (!IsA(jexpr->path_spec, Const)) + return true; + + cnst = castNode(Const, jexpr->path_spec); + + Assert(cnst->consttype == JSONPATHOID); + if (cnst->constisnull) + return false; + + return jspIsMutable(DatumGetJsonPathP(cnst->constvalue), + jexpr->passing_names, jexpr->passing_values); + } + if (IsA(node, SQLValueFunction)) { /* all variants of SQLValueFunction are stable */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 8f239de65c..4f5c44e063 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -278,6 +278,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MergeWhenClause *mergewhen; struct KeyActions *keyactions; struct KeyAction *keyaction; + JsonBehavior *jsbehavior; + JsonQuotes js_quotes; } %type <node> stmt toplevel_stmt schema_stmt routine_body_stmt @@ -652,13 +654,23 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_output_clause_opt json_name_and_value json_aggregate_func + json_api_common_syntax + json_argument %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt + json_arguments %type <ival> json_encoding_clause_opt json_predicate_type_constraint + json_wrapper_behavior + +%type <jsbehavior> json_value_behavior + json_query_behavior + json_exists_behavior + +%type <js_quotes> json_quotes_clause_opt %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt @@ -699,7 +711,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT - COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT + COMMITTED COMPRESSION CONCURRENTLY CONDITIONAL CONFIGURATION CONFLICT CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY COST CREATE CROSS CSV CUBE CURRENT_P CURRENT_CATALOG CURRENT_DATE CURRENT_ROLE CURRENT_SCHEMA @@ -710,8 +722,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P DOUBLE_P DROP - EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT - EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION + EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE + EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION EXTENSION EXTERNAL EXTRACT FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR @@ -726,10 +738,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION - JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG - JSON_SCALAR JSON_SERIALIZE + JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE - KEY KEYS + KEY KEYS KEEP LABEL LANGUAGE LARGE_P LAST_P LATERAL_P LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL @@ -743,7 +755,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC - OBJECT_P OF OFF OFFSET OIDS OLD ON ONLY OPERATOR OPTION OPTIONS OR + OBJECT_P OF OFF OFFSET OIDS OLD OMIT ON ONLY OPERATOR OPTION OPTIONS OR ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER @@ -752,7 +764,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION - QUOTE + QUOTE QUOTES RANGE READ REAL REASSIGN RECHECK RECURSIVE REF_P REFERENCES REFERENCING REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA @@ -763,7 +775,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); SEQUENCE SEQUENCES SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P - START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P + START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRING_P STRIP_P SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN @@ -771,7 +783,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); TREAT TRIGGER TRIM TRUE_P TRUNCATE TRUSTED TYPE_P TYPES_P - UESCAPE UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN + UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING @@ -15668,6 +15680,192 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } + | JSON_QUERY '(' + json_api_common_syntax + json_output_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_output_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_output_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_error = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_output_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->on_error = $10; + n->location = @1; + $$ = (Node *) n; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->location = @1; + $$ = (Node *) p; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + json_exists_behavior ON ERROR_P + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->on_error = $5; + p->location = @1; + $$ = (Node *) p; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->on_error = $8; + n->location = @1; + $$ = (Node *) n; + } ; @@ -16394,6 +16592,72 @@ opt_asymmetric: ASYMMETRIC ; /* SQL/JSON support */ +json_api_common_syntax: + json_value_expr ',' a_expr /* i.e. a json_path */ + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + + | json_value_expr ',' a_expr /* i.e. a json_path */ + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = $5; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = $7; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_arguments: + json_argument { $$ = list_make1($1); } + | json_arguments ',' json_argument { $$ = lappend($1, $3); } + ; + +json_argument: + json_value_expr AS ColLabel + { + JsonArgument *n = makeNode(JsonArgument); + + n->val = (JsonValueExpr *) $1; + n->name = $3; + $$ = (Node *) n; + } + ; + json_value_expr: a_expr json_format_clause_opt { @@ -16417,6 +16681,50 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; +json_query_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + | EMPTY_P ARRAY { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + | EMPTY_P OBJECT_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + /* non-standard, for Oracle compatibility only */ + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + ; + +json_exists_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | TRUE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_TRUE, NULL); } + | FALSE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_FALSE, NULL); } + | UNKNOWN { $$ = makeJsonBehavior(JSON_BEHAVIOR_UNKNOWN, NULL); } + ; + +json_value_behavior: + NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + ; + +/* ARRAY is a noise word */ +json_wrapper_behavior: + WITHOUT WRAPPER { $$ = JSW_NONE; } + | WITHOUT ARRAY WRAPPER { $$ = JSW_NONE; } + | WITH WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL ARRAY WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL WRAPPER { $$ = JSW_UNCONDITIONAL; } + | /* empty */ { $$ = JSW_NONE; } + ; + +json_quotes_clause_opt: + KEEP QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_KEEP; } + | KEEP QUOTES { $$ = JS_QUOTES_KEEP; } + | OMIT QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_OMIT; } + | OMIT QUOTES { $$ = JS_QUOTES_OMIT; } + | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } + ; + json_returning_clause_opt: RETURNING Typename { @@ -17033,6 +17341,7 @@ unreserved_keyword: | COMMIT | COMMITTED | COMPRESSION + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17069,10 +17378,12 @@ unreserved_keyword: | DOUBLE_P | DROP | EACH + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17122,6 +17433,7 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION + | KEEP | KEY | KEYS | LABEL @@ -17168,6 +17480,7 @@ unreserved_keyword: | OFF | OIDS | OLD + | OMIT | OPERATOR | OPTION | OPTIONS @@ -17198,6 +17511,7 @@ unreserved_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REASSIGN @@ -17257,6 +17571,7 @@ unreserved_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUPPORT @@ -17279,6 +17594,7 @@ unreserved_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNKNOWN | UNLISTEN @@ -17339,10 +17655,13 @@ col_name_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE | LEAST | NATIONAL | NCHAR @@ -17575,6 +17894,7 @@ bare_label_keyword: | COMMITTED | COMPRESSION | CONCURRENTLY + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17627,11 +17947,13 @@ bare_label_keyword: | DROP | EACH | ELSE + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | END_P | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17701,10 +18023,14 @@ bare_label_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE + | KEEP | KEY | KEYS | LABEL @@ -17765,6 +18091,7 @@ bare_label_keyword: | OFF | OIDS | OLD + | OMIT | ONLY | OPERATOR | OPTION @@ -17802,6 +18129,7 @@ bare_label_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REAL @@ -17870,6 +18198,7 @@ bare_label_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUBSTRING @@ -17904,6 +18233,7 @@ bare_label_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNIQUE | UNKNOWN diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c index 9f6afc351c..692e5d1225 100644 --- a/src/backend/parser/parse_collate.c +++ b/src/backend/parser/parse_collate.c @@ -691,6 +691,13 @@ assign_collations_walker(Node *node, assign_collations_context *context) &loccontext); } break; + case T_JsonExpr: + + /* + * Context item and PASSING arguments are already + * marked with collations in parse_expr.c. + */ + break; default: /* diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index b46e06237b..f6a7ba5ef5 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -90,6 +90,8 @@ static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); static Node *transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr); +static Node *transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve); +static Node *transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *p); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -353,6 +355,14 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); break; + case T_JsonFuncExpr: + result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); + break; + + case T_JsonValueExpr: + result = transformJsonValueExpr(pstate, (JsonValueExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3238,10 +3248,10 @@ makeCaseTestExpr(Node *expr) * default format otherwise. */ static Node * -transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, - char *constructName, - JsonFormatType default_format, - Oid targettype) +transformJsonValueExprInternal(ParseState *pstate, JsonValueExpr *ve, + char *constructName, + JsonFormatType default_format, + Oid targettype, bool isarg) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3278,6 +3288,35 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, else format = ve->format->format_type; } + else if (isarg) + { + /* Pass SQL/JSON item types directly without conversion to json[b]. */ + switch (exprtype) + { + case TEXTOID: + case NUMERICOID: + case BOOLOID: + case INT2OID: + case INT4OID: + case INT8OID: + case FLOAT4OID: + case FLOAT8OID: + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + return expr; + + default: + if (typcategory == TYPCATEGORY_STRING) + return expr; + /* else convert argument to json[b] type */ + break; + } + + format = default_format; + } else if (exprtype == JSONOID || exprtype == JSONBOID) format = JS_FORMAT_DEFAULT; /* do not format json[b] types */ else @@ -3289,7 +3328,8 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, Node *coerced; bool cast_is_needed = OidIsValid(targettype); - if (!cast_is_needed && + if (!isarg && + !cast_is_needed && exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), @@ -3354,6 +3394,20 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, return expr; } +/* Wrapper with an interface for use in transformExpr() */ +static Node * +transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve) +{ + /* + * A bit bogus to use "JSON()" here for the parent construct's name but + * there's no good way to know the enclosing JSON expression when called + * through transformExpr(). + */ + return transformJsonValueExprInternal(pstate, ve, "JSON()", + JS_FORMAT_DEFAULT, + InvalidOid, false); +} + /* * Checks specified output format for its applicability to the target type. */ @@ -3613,10 +3667,12 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) { JsonKeyValue *kv = castNode(JsonKeyValue, lfirst(lc)); Node *key = transformExprRecurse(pstate, (Node *) kv->key); - Node *val = transformJsonValueExpr(pstate, kv->value, - "JSON_OBJECT()", - JS_FORMAT_DEFAULT, - InvalidOid); + Node *val = transformJsonValueExprInternal(pstate, + kv->value, + "JSON_OBJECT()", + JS_FORMAT_DEFAULT, + InvalidOid, + false); args = lappend(args, key); args = lappend(args, val); @@ -3795,8 +3851,10 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) Oid aggtype; key = transformExprRecurse(pstate, (Node *) agg->arg->key); - val = transformJsonValueExpr(pstate, agg->arg->value, "JSON_OBJECTAGG()", - JS_FORMAT_DEFAULT, InvalidOid); + val = transformJsonValueExprInternal(pstate, agg->arg->value, + "JSON_OBJECTAGG()", + JS_FORMAT_DEFAULT, + InvalidOid, false); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3852,8 +3910,8 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) Oid aggfnoid; Oid aggtype; - arg = transformJsonValueExpr(pstate, agg->arg, "JSON_ARRAYAGG()", - JS_FORMAT_DEFAULT, InvalidOid); + arg = transformJsonValueExprInternal(pstate, agg->arg, "JSON_ARRAYAGG()", + JS_FORMAT_DEFAULT, InvalidOid, false); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3899,10 +3957,10 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) foreach(lc, ctor->exprs) { JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); - Node *val = transformJsonValueExpr(pstate, jsval, - "JSON_ARRAY()", - JS_FORMAT_DEFAULT, - InvalidOid); + Node *val = transformJsonValueExprInternal(pstate, jsval, + "JSON_ARRAY()", + JS_FORMAT_DEFAULT, + InvalidOid, false); args = lappend(args, val); } @@ -4057,8 +4115,9 @@ transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) * Coerce argument to target type using CAST for compatibility with PG * function-like CASTs. */ - arg = transformJsonValueExpr(pstate, jsexpr->expr, "JSON()", - JS_FORMAT_JSON, returning->typid); + arg = transformJsonValueExprInternal(pstate, jsexpr->expr, "JSON()", + JS_FORMAT_JSON, returning->typid, + false); } return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, @@ -4089,10 +4148,10 @@ transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *jsexpr) static Node * transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) { - Node *arg = transformJsonValueExpr(pstate, expr->expr, - "JSON_SERIALIZE()", - JS_FORMAT_JSON, - InvalidOid); + Node *arg = transformJsonValueExprInternal(pstate, expr->expr, + "JSON_SERIALIZE()", + JS_FORMAT_JSON, + InvalidOid, false); JsonReturning *returning; if (expr->output) @@ -4127,3 +4186,438 @@ transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), NULL, returning, false, false, expr->location); } + +/* + * Transform a JSON PASSING clause. + */ +static void +transformJsonPassingArgs(ParseState *pstate, JsonFormatType format, List *args, + List **passing_values, List **passing_names) +{ + ListCell *lc; + + *passing_values = NIL; + *passing_names = NIL; + + foreach(lc, args) + { + JsonArgument *arg = castNode(JsonArgument, lfirst(lc)); + Node *expr = transformJsonValueExprInternal(pstate, arg->val, + "JSON PASSING()", + format, InvalidOid, + true); + + assign_expr_collations(pstate, expr); + + *passing_values = lappend(*passing_values, expr); + *passing_names = lappend(*passing_names, makeString(arg->name)); + } +} + +/* + * Transform a JSON BEHAVIOR clause. + */ +static JsonBehavior * +transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior, + JsonBehaviorType default_behavior) +{ + JsonBehaviorType behavior_type = default_behavior; + Node *default_expr = NULL; + + if (behavior) + { + behavior_type = behavior->btype; + if (behavior_type == JSON_BEHAVIOR_DEFAULT) + default_expr = transformExprRecurse(pstate, behavior->default_expr); + } + return makeJsonBehavior(behavior_type, default_expr); +} + +/* + * Common code for JSON_VALUE, JSON_QUERY, JSON_EXISTS transformation + * into a JsonExpr node. + */ +static JsonExpr * +transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = makeNode(JsonExpr); + Node *pathspec; + JsonFormatType format; + char *constructName; + + if (func->common->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("JSON_TABLE path name is not allowed here"), + parser_errposition(pstate, func->location))); + + jsexpr->location = func->location; + jsexpr->op = func->op; + switch (jsexpr->op) + { + case JSON_VALUE_OP: + constructName = "JSON_VALUE()"; + break; + case JSON_QUERY_OP: + constructName = "JSON_QUERY()"; + break; + case JSON_EXISTS_OP: + constructName = "JSON_EXISTS()"; + break; + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); + break; + } + jsexpr->formatted_expr = transformJsonValueExprInternal(pstate, + func->common->expr, + constructName, + JS_FORMAT_DEFAULT, + InvalidOid, false); + + assign_expr_collations(pstate, jsexpr->formatted_expr); + + /* format is determined by context item type */ + format = exprType(jsexpr->formatted_expr) == JSONBOID ? JS_FORMAT_JSONB : JS_FORMAT_JSON; + + jsexpr->result_coercion = NULL; + jsexpr->omit_quotes = false; + + jsexpr->format = func->common->expr->format; + + pathspec = transformExprRecurse(pstate, func->common->pathspec); + + jsexpr->path_spec = + coerce_to_target_type(pstate, pathspec, exprType(pathspec), + JSONPATHOID, -1, + COERCION_EXPLICIT, COERCE_IMPLICIT_CAST, + exprLocation(pathspec)); + if (!jsexpr->path_spec) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("JSON path expression must be type %s, not type %s", + "jsonpath", format_type_be(exprType(pathspec))), + parser_errposition(pstate, exprLocation(pathspec)))); + + /* transform and coerce to json[b] passing arguments */ + transformJsonPassingArgs(pstate, format, func->common->passing, + &jsexpr->passing_values, &jsexpr->passing_names); + + if (func->op != JSON_EXISTS_OP) + jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, + JSON_BEHAVIOR_NULL); + + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + func->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : + JSON_BEHAVIOR_NULL); + + return jsexpr; +} + +/* + * Assign default JSON returning type from the specified format or from + * the context item type. + */ +static void +assignDefaultJsonReturningType(Node *context_item, JsonFormat *context_format, + JsonReturning *ret) +{ + bool is_jsonb; + + ret->format = copyObject(context_format); + + if (ret->format->format_type == JS_FORMAT_DEFAULT) + is_jsonb = exprType(context_item) == JSONBOID; + else + is_jsonb = ret->format->format_type == JS_FORMAT_JSONB; + + ret->typid = is_jsonb ? JSONBOID : JSONOID; + ret->typmod = -1; +} + +/* + * Try to coerce expression to the output type or + * use json_populate_type() for composite, array and domain types or + * use coercion via I/O. + */ +static JsonCoercion * +coerceJsonExpr(ParseState *pstate, Node *expr, const JsonReturning *returning) +{ + char typtype; + JsonCoercion *coercion = makeNode(JsonCoercion); + + coercion->expr = coerceJsonFuncExpr(pstate, expr, returning, false); + + if (coercion->expr) + { + if (coercion->expr == expr) + coercion->expr = NULL; + + return coercion; + } + + typtype = get_typtype(returning->typid); + + if (returning->typid == RECORDOID || + typtype == TYPTYPE_COMPOSITE || + typtype == TYPTYPE_DOMAIN || + type_is_array(returning->typid)) + coercion->via_populate = true; + else + coercion->via_io = true; + + return coercion; +} + +/* + * Transform a JSON output clause of JSON_VALUE and JSON_QUERY. + */ +static void +transformJsonFuncExprOutput(ParseState *pstate, JsonFuncExpr *func, + JsonExpr *jsexpr) +{ + Node *expr = jsexpr->formatted_expr; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + /* JSON_VALUE returns text by default */ + if (func->op == JSON_VALUE_OP && !OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = TEXTOID; + jsexpr->returning->typmod = -1; + } + + if (OidIsValid(jsexpr->returning->typid)) + { + JsonReturning ret; + + if (func->op == JSON_VALUE_OP && + jsexpr->returning->typid != JSONOID && + jsexpr->returning->typid != JSONBOID) + { + /* Forced coercion via I/O for JSON_VALUE for non-JSON types */ + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = NULL; + jsexpr->result_coercion->via_io = true; + return; + } + + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, &ret); + + if (ret.typid != jsexpr->returning->typid || + ret.typmod != jsexpr->returning->typmod) + { + Node *placeholder = makeCaseTestExpr(expr); + + Assert(((CaseTestExpr *) placeholder)->typeId == ret.typid); + Assert(((CaseTestExpr *) placeholder)->typeMod == ret.typmod); + + jsexpr->result_coercion = coerceJsonExpr(pstate, placeholder, + jsexpr->returning); + } + } + else + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, + jsexpr->returning); +} + +/* + * Coerce an expression in JSON DEFAULT behavior to the target output type. + */ +static Node * +coerceDefaultJsonExpr(ParseState *pstate, JsonExpr *jsexpr, Node *defexpr) +{ + int location; + Oid exprtype; + + if (!defexpr) + return NULL; + + exprtype = exprType(defexpr); + location = exprLocation(defexpr); + + if (location < 0) + location = jsexpr->location; + + defexpr = coerce_to_target_type(pstate, + defexpr, + exprtype, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!defexpr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast DEFAULT expression type %s to %s", + format_type_be(exprtype), + format_type_be(jsexpr->returning->typid)), + parser_errposition(pstate, location))); + + return defexpr; +} + +/* + * Initialize SQL/JSON item coercion from the SQL type "typid" to the target + * "returning" type. + */ +static JsonCoercion * +initJsonItemCoercion(ParseState *pstate, Oid typid, + const JsonReturning *returning) +{ + Node *expr; + + if (typid == UNKNOWNOID) + { + expr = (Node *) makeNullConst(UNKNOWNOID, -1, InvalidOid); + } + else + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + + placeholder->typeId = typid; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + expr = (Node *) placeholder; + } + + return coerceJsonExpr(pstate, expr, returning); +} + +static void +initJsonItemCoercions(ParseState *pstate, JsonItemCoercions *coercions, + const JsonReturning *returning, Oid contextItemTypeId) +{ + struct + { + JsonCoercion **coercion; + Oid typid; + } *p, + coercionTypids[] = + { + {&coercions->null, UNKNOWNOID}, + {&coercions->string, TEXTOID}, + {&coercions->numeric, NUMERICOID}, + {&coercions->boolean, BOOLOID}, + {&coercions->date, DATEOID}, + {&coercions->time, TIMEOID}, + {&coercions->timetz, TIMETZOID}, + {&coercions->timestamp, TIMESTAMPOID}, + {&coercions->timestamptz, TIMESTAMPTZOID}, + {&coercions->composite, contextItemTypeId}, + {NULL, InvalidOid} + }; + + for (p = coercionTypids; p->coercion; p++) + *p->coercion = initJsonItemCoercion(pstate, p->typid, returning); +} + +/* + * Transform JSON_VALUE, JSON_QUERY, JSON_EXISTS functions into a JsonExpr node. + */ +static Node * +transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = transformJsonExprCommon(pstate, func); + const char *func_name = NULL; + Node *contextItemExpr = jsexpr->formatted_expr; + + switch (func->op) + { + case JSON_VALUE_OP: + func_name = "JSON_VALUE"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->coercions = makeNode(JsonItemCoercions); + initJsonItemCoercions(pstate, jsexpr->coercions, jsexpr->returning, + exprType(contextItemExpr)); + + break; + + case JSON_QUERY_OP: + func_name = "JSON_QUERY"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->wrapper = func->wrapper; + jsexpr->omit_quotes = func->omit_quotes; + + break; + + case JSON_EXISTS_OP: + func_name = "JSON_EXISTS"; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + if (!OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = BOOLOID; + jsexpr->returning->typmod = -1; + } + else if (jsexpr->returning->typid != BOOLOID) + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + int location = exprLocation((Node *) jsexpr); + + placeholder->typeId = BOOLOID; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = + coerce_to_target_type(pstate, (Node *) placeholder, BOOLOID, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!jsexpr->result_coercion->expr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(BOOLOID), + format_type_be(jsexpr->returning->typid)), + parser_coercion_errposition(pstate, location, (Node *) jsexpr))); + + if (jsexpr->result_coercion->expr == (Node *) placeholder) + jsexpr->result_coercion->expr = NULL; + } + break; + } + + if (exprType(contextItemExpr) != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("%s() is not yet implemented for the json type", func_name), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + return (Node *) jsexpr; +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 520d4f2a23..4f94fc69d6 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1979,6 +1979,21 @@ FigureColnameInternal(Node *node, char **name) /* make JSON_ARRAYAGG act like a regular function */ *name = "json_arrayagg"; return 2; + case T_JsonFuncExpr: + /* make SQL/JSON functions act like a regular function */ + switch (((JsonFuncExpr *) node)->op) + { + case JSON_QUERY_OP: + *name = "json_query"; + return 2; + case JSON_VALUE_OP: + *name = "json_value"; + return 2; + case JSON_EXISTS_OP: + *name = "json_exists"; + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index e6246dc44b..f6015debbb 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -4444,6 +4444,50 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, } } +/* + * Parses the datetime format string in 'fmt_str' and returns true if it + * contains a timezone specifier, false if not. + */ +bool +datetime_format_has_tz(const char *fmt_str) +{ + bool incache; + int fmt_len = strlen(fmt_str); + int result; + FormatNode *format; + + if (fmt_len > DCH_CACHE_SIZE) + { + /* + * Allocate new memory if format picture is bigger than static cache + * and do not use cache (call parser always) + */ + incache = false; + + format = (FormatNode *) palloc((fmt_len + 1) * sizeof(FormatNode)); + + parse_format(format, fmt_str, DCH_keywords, + DCH_suff, DCH_index, DCH_FLAG, NULL); + } + else + { + /* + * Use cache buffers + */ + DCHCacheEntry *ent = DCH_cache_fetch(fmt_str, false); + + incache = true; + format = ent->format; + } + + result = DCH_datetime_type(format); + + if (!incache) + pfree(format); + + return result & DCH_ZONED; +} + /* * do_to_timestamp: shared code for to_timestamp and to_date * diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index 7a91177a55..100dfa9c3a 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -2265,3 +2265,65 @@ jsonb_float8(PG_FUNCTION_ARGS) PG_RETURN_DATUM(retValue); } + +/* + * Construct an empty array jsonb. + */ +Jsonb * +JsonbMakeEmptyArray(void) +{ + JsonbValue jbv; + + jbv.type = jbvArray; + jbv.val.array.elems = NULL; + jbv.val.array.nElems = 0; + jbv.val.array.rawScalar = false; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Construct an empty object jsonb. + */ +Jsonb * +JsonbMakeEmptyObject(void) +{ + JsonbValue jbv; + + jbv.type = jbvObject; + jbv.val.object.pairs = NULL; + jbv.val.object.nPairs = 0; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Convert jsonb to a C-string stripping quotes from scalar strings. + */ +char * +JsonbUnquote(Jsonb *jb) +{ + if (JB_ROOT_IS_SCALAR(jb)) + { + JsonbValue v; + + (void) JsonbExtractScalar(&jb->root, &v); + + if (v.type == jbvString) + return pnstrdup(v.val.string.val, v.val.string.len); + else if (v.type == jbvBool) + return pstrdup(v.val.boolean ? "true" : "false"); + else if (v.type == jbvNumeric) + return DatumGetCString(DirectFunctionCall1(numeric_out, + PointerGetDatum(v.val.numeric))); + else if (v.type == jbvNull) + return pstrdup("null"); + else + { + elog(ERROR, "unrecognized jsonb value type %d", v.type); + return NULL; + } + } + else + return JsonbToCString(NULL, &jb->root, VARSIZE(jb)); +} diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index 70cb922e6b..cbc6b94fc2 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -264,6 +264,7 @@ typedef struct PopulateArrayContext int *dims; /* dimensions */ int *sizes; /* current dimension counters */ int ndims; /* number of dimensions */ + Node *escontext; /* For soft-error capture */ } PopulateArrayContext; /* state for populate_array_json() */ @@ -441,12 +442,13 @@ static void JsValueToJsObject(JsValue *jsv, JsObject *jso); static Datum populate_composite(CompositeIOData *io, Oid typid, const char *colname, MemoryContext mcxt, HeapTupleHeader defaultval, JsValue *jsv, bool isnull); -static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv); +static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext); static void prepare_column_cache(ColumnIOData *column, Oid typid, int32 typmod, MemoryContext mcxt, bool need_scalar); static Datum populate_record_field(ColumnIOData *col, Oid typid, int32 typmod, const char *colname, MemoryContext mcxt, Datum defaultval, - JsValue *jsv, bool *isnull); + JsValue *jsv, bool *isnull, Node *escontext); static RecordIOData *allocate_record_info(MemoryContext mcxt, int ncolumns); static bool JsObjectGetField(JsObject *obj, char *field, JsValue *jsv); static void populate_recordset_record(PopulateRecordsetState *state, JsObject *obj); @@ -458,7 +460,8 @@ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims); static void populate_array_check_dimension(PopulateArrayContext *ctx, int ndim); static void populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv); static Datum populate_array(ArrayIOData *aio, const char *colname, - MemoryContext mcxt, JsValue *jsv); + MemoryContext mcxt, JsValue *jsv, Node *escontext, + bool *isnull); static Datum populate_domain(DomainIOData *io, Oid typid, const char *colname, MemoryContext mcxt, JsValue *jsv, bool isnull); @@ -2483,12 +2486,12 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) if (ndim <= 0) { if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the value of key \"%s\".", ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"))); } @@ -2505,13 +2508,13 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) appendStringInfo(&indices, "[%d]", ctx->sizes[i]); if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s of key \"%s\".", indices.data, ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s.", @@ -2519,7 +2522,11 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) } } -/* set the number of dimensions of the populated array when it becomes known */ +/* + * Set the number of dimensions of the populated array when it becomes known. + * + * Returns without doing anything if the input (ndims) is erratic. + */ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) { @@ -2530,6 +2537,10 @@ populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) if (ndims <= 0) populate_array_report_expected_array(ctx, ndims); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + ctx->ndims = ndims; ctx->dims = palloc(sizeof(int) * ndims); ctx->sizes = palloc0(sizeof(int) * ndims); @@ -2547,12 +2558,16 @@ populate_array_check_dimension(PopulateArrayContext *ctx, int ndim) if (ctx->dims[ndim] == -1) ctx->dims[ndim] = dim; /* assign dimension if not yet known */ else if (ctx->dims[ndim] != dim) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("malformed JSON array"), errdetail("Multidimensional arrays must have " "sub-arrays with matching dimensions."))); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + /* reset the current array dimension size counter */ ctx->sizes[ndim] = 0; @@ -2572,7 +2587,10 @@ populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv) ctx->aio->element_type, ctx->aio->element_typmod, NULL, ctx->mcxt, PointerGetDatum(NULL), - jsv, &element_isnull); + jsv, &element_isnull, ctx->escontext); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; accumArrayResult(ctx->astate, element, element_isnull, ctx->aio->element_type, ctx->acxt); @@ -2593,6 +2611,10 @@ populate_array_object_start(void *_state) else if (ndim < state->ctx->ndims) populate_array_report_expected_array(state->ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2610,6 +2632,10 @@ populate_array_array_end(void *_state) if (ndim < ctx->ndims) populate_array_check_dimension(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2685,6 +2711,10 @@ populate_array_scalar(void *_state, char *token, JsonTokenType tokentype) else if (ndim < ctx->ndims) populate_array_report_expected_array(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + if (ndim == ctx->ndims) { /* remember the scalar element token */ @@ -2714,7 +2744,13 @@ populate_array_json(PopulateArrayContext *ctx, char *json, int len) sem.array_element_end = populate_array_element_end; sem.scalar = populate_array_scalar; - pg_parse_json_or_ereport(state.lex, &sem); + if (!pg_parse_json_or_errsave(state.lex, &sem, ctx->escontext)) + { + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + pfree(state.lex); + return; + } /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2739,10 +2775,15 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ check_stack_depth(); - if (jbv->type != jbvBinary || !JsonContainerIsArray(jbc)) + if (jbv->type != jbvBinary || + !JsonContainerIsArray(jbc) || + JsonContainerIsScalar(jbc)) + { populate_array_report_expected_array(ctx, ndim - 1); - - Assert(!JsonContainerIsScalar(jbc)); + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + return; + } it = JsonbIteratorInit(jbc); @@ -2761,7 +2802,12 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ (tok == WJB_ELEM && (val.type != jbvBinary || !JsonContainerIsArray(val.val.binary.data))))) + { populate_array_assign_ndims(ctx, ndim); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + } jsv.is_json = false; jsv.val.jsonb = &val; @@ -2779,6 +2825,9 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ { /* populate child sub-array */ populate_array_dim_jsonb(ctx, &val, ndim + 1); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2796,12 +2845,18 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ Assert(tok == WJB_DONE && !it); } -/* recursively populate an array from json/jsonb */ +/* + * Recursively populate an array from json/jsonb + * + * *isnull is set to true if an error is reported during parsing. + */ static Datum populate_array(ArrayIOData *aio, const char *colname, MemoryContext mcxt, - JsValue *jsv) + JsValue *jsv, + Node *escontext, + bool *isnull) { PopulateArrayContext ctx; Datum result; @@ -2816,6 +2871,7 @@ populate_array(ArrayIOData *aio, ctx.ndims = 0; /* unknown yet */ ctx.dims = NULL; ctx.sizes = NULL; + ctx.escontext = escontext; if (jsv->is_json) populate_array_json(&ctx, jsv->val.json.str, @@ -2824,7 +2880,16 @@ populate_array(ArrayIOData *aio, else { populate_array_dim_jsonb(&ctx, jsv->val.jsonb, 1); - ctx.dims[0] = ctx.sizes[0]; + /* Nothing to do on an error. */ + if (!SOFT_ERROR_OCCURRED(ctx.escontext)) + ctx.dims[0] = ctx.sizes[0]; + } + + /* Nothing to return if an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx.escontext)) + { + *isnull = true; + return (Datum) 0; } Assert(ctx.ndims > 0); @@ -2841,6 +2906,7 @@ populate_array(ArrayIOData *aio, pfree(ctx.sizes); pfree(lbs); + *isnull = false; return result; } @@ -2956,7 +3022,8 @@ populate_composite(CompositeIOData *io, /* populate non-null scalar value from json/jsonb value */ static Datum -populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) +populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext) { Datum res; char *str = NULL; @@ -3027,7 +3094,11 @@ populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) elog(ERROR, "unrecognized jsonb type: %d", (int) jbv->type); } - res = InputFunctionCall(&io->typiofunc, str, io->typioparam, typmod); + if (!InputFunctionCallSafe(&io->typiofunc, str, io->typioparam, typmod, + escontext, &res)) + { + res = (Datum) 0; + } /* free temporary buffer */ if (str != json) @@ -3053,7 +3124,7 @@ populate_domain(DomainIOData *io, res = populate_record_field(io->base_io, io->base_typid, io->base_typmod, colname, mcxt, PointerGetDatum(NULL), - jsv, &isnull); + jsv, &isnull, NULL); Assert(!isnull); } @@ -3158,7 +3229,8 @@ populate_record_field(ColumnIOData *col, MemoryContext mcxt, Datum defaultval, JsValue *jsv, - bool *isnull) + bool *isnull, + Node *escontext) { TypeCat typcat; @@ -3191,10 +3263,12 @@ populate_record_field(ColumnIOData *col, switch (typcat) { case TYPECAT_SCALAR: - return populate_scalar(&col->scalar_io, typid, typmod, jsv); + return populate_scalar(&col->scalar_io, typid, typmod, jsv, + escontext); case TYPECAT_ARRAY: - return populate_array(&col->io.array, colname, mcxt, jsv); + return populate_array(&col->io.array, colname, mcxt, jsv, + escontext, isnull); case TYPECAT_COMPOSITE: case TYPECAT_COMPOSITE_DOMAIN: @@ -3215,6 +3289,53 @@ populate_record_field(ColumnIOData *col, } } +/* recursively populate specified type from a json/jsonb value */ +Datum +json_populate_type(Datum json_val, Oid json_type, Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext) +{ + JsValue jsv = {0}; + JsonbValue jbv; + + jsv.is_json = json_type == JSONOID; + + if (*isnull) + { + if (jsv.is_json) + jsv.val.json.str = NULL; + else + jsv.val.jsonb = NULL; + } + else if (jsv.is_json) + { + text *json = DatumGetTextPP(json_val); + + jsv.val.json.str = VARDATA_ANY(json); + jsv.val.json.len = VARSIZE_ANY_EXHDR(json); + jsv.val.json.type = JSON_TOKEN_INVALID; /* not used in + * populate_composite() */ + } + else + { + Jsonb *jsonb = DatumGetJsonbP(json_val); + + jsv.val.jsonb = &jbv; + + /* fill binary jsonb value pointing to jb */ + jbv.type = jbvBinary; + jbv.val.binary.data = &jsonb->root; + jbv.val.binary.len = VARSIZE(jsonb) - VARHDRSZ; + } + + if (!*cache) + *cache = MemoryContextAllocZero(mcxt, sizeof(ColumnIOData)); + + return populate_record_field(*cache, typid, typmod, NULL, mcxt, + PointerGetDatum(NULL), &jsv, isnull, + escontext); +} + static RecordIOData * allocate_record_info(MemoryContext mcxt, int ncolumns) { @@ -3356,7 +3477,8 @@ populate_record(TupleDesc tupdesc, mcxt, nulls[i] ? (Datum) 0 : values[i], &field, - &nulls[i]); + &nulls[i], + NULL); } res = heap_form_tuple(tupdesc, values, nulls); diff --git a/src/backend/utils/adt/jsonpath.c b/src/backend/utils/adt/jsonpath.c index 7891fde310..5194d0d91f 100644 --- a/src/backend/utils/adt/jsonpath.c +++ b/src/backend/utils/adt/jsonpath.c @@ -68,7 +68,9 @@ #include "libpq/pqformat.h" #include "nodes/miscnodes.h" #include "miscadmin.h" +#include "nodes/nodeFuncs.h" #include "utils/builtins.h" +#include "utils/formatting.h" #include "utils/json.h" #include "utils/jsonpath.h" @@ -1109,3 +1111,256 @@ jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, return true; } + +/* SQL/JSON datatype status: */ +typedef enum JsonPathDatatypeStatus +{ + jpdsNonDateTime, /* null, bool, numeric, string, array, object */ + jpdsUnknownDateTime, /* unknown datetime type */ + jpdsDateTimeZoned, /* timetz, timestamptz */ + jpdsDateTimeNonZoned /* time, timestamp, date */ +} JsonPathDatatypeStatus; + +/* Context for jspIsMutableWalker() */ +typedef struct JsonPathMutableContext +{ + List *varnames; /* list of variable names */ + List *varexprs; /* list of variable expressions */ + JsonPathDatatypeStatus current; /* status of @ item */ + bool lax; /* jsonpath is lax or strict */ + bool mutable; /* resulting mutability status */ +} JsonPathMutableContext; + +/* + * Recursive walker for jspIsMutable() + */ +static JsonPathDatatypeStatus +jspIsMutableWalker(JsonPathItem *jpi, JsonPathMutableContext *cxt) +{ + JsonPathItem next; + JsonPathDatatypeStatus status = jpdsNonDateTime; + + while (!cxt->mutable) + { + JsonPathItem arg; + JsonPathDatatypeStatus leftStatus; + JsonPathDatatypeStatus rightStatus; + + switch (jpi->type) + { + case jpiRoot: + Assert(status == jpdsNonDateTime); + break; + + case jpiCurrent: + Assert(status == jpdsNonDateTime); + status = cxt->current; + break; + + case jpiFilter: + { + JsonPathDatatypeStatus prevStatus = cxt->current; + + cxt->current = status; + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + + cxt->current = prevStatus; + break; + } + + case jpiVariable: + { + int32 len; + const char *name = jspGetString(jpi, &len); + ListCell *lc1; + ListCell *lc2; + + Assert(status == jpdsNonDateTime); + + forboth(lc1, cxt->varnames, lc2, cxt->varexprs) + { + String *varname = lfirst_node(String, lc1); + Node *varexpr = lfirst(lc2); + + if (strncmp(varname->sval, name, len)) + continue; + + switch (exprType(varexpr)) + { + case DATEOID: + case TIMEOID: + case TIMESTAMPOID: + status = jpdsDateTimeNonZoned; + break; + + case TIMETZOID: + case TIMESTAMPTZOID: + status = jpdsDateTimeZoned; + break; + + default: + status = jpdsNonDateTime; + break; + } + + break; + } + break; + } + + case jpiEqual: + case jpiNotEqual: + case jpiLess: + case jpiGreater: + case jpiLessOrEqual: + case jpiGreaterOrEqual: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + leftStatus = jspIsMutableWalker(&arg, cxt); + + jspGetRightArg(jpi, &arg); + rightStatus = jspIsMutableWalker(&arg, cxt); + + /* + * Comparison of datetime type with different timezone status + * is mutable. + */ + if (leftStatus != jpdsNonDateTime && + rightStatus != jpdsNonDateTime && + (leftStatus == jpdsUnknownDateTime || + rightStatus == jpdsUnknownDateTime || + leftStatus != rightStatus)) + cxt->mutable = true; + break; + + case jpiNot: + case jpiIsUnknown: + case jpiExists: + case jpiPlus: + case jpiMinus: + Assert(status == jpdsNonDateTime); + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiAnd: + case jpiOr: + case jpiAdd: + case jpiSub: + case jpiMul: + case jpiDiv: + case jpiMod: + case jpiStartsWith: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + jspGetRightArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiIndexArray: + for (int i = 0; i < jpi->content.array.nelems; i++) + { + JsonPathItem from; + JsonPathItem to; + + if (jspGetArraySubscript(jpi, &from, &to, i)) + jspIsMutableWalker(&to, cxt); + + jspIsMutableWalker(&from, cxt); + } + /* FALLTHROUGH */ + + case jpiAnyArray: + if (!cxt->lax) + status = jpdsNonDateTime; + break; + + case jpiAny: + if (jpi->content.anybounds.first > 0) + status = jpdsNonDateTime; + break; + + case jpiDatetime: + if (jpi->content.arg) + { + char *template; + + jspGetArg(jpi, &arg); + if (arg.type != jpiString) + { + status = jpdsNonDateTime; + break; /* there will be runtime error */ + } + + template = jspGetString(&arg, NULL); + if (datetime_format_has_tz(template)) + status = jpdsDateTimeZoned; + else + status = jpdsDateTimeNonZoned; + } + else + { + status = jpdsUnknownDateTime; + } + break; + + case jpiLikeRegex: + Assert(status == jpdsNonDateTime); + jspInitByBuffer(&arg, jpi->base, jpi->content.like_regex.expr); + jspIsMutableWalker(&arg, cxt); + break; + + /* literals */ + case jpiNull: + case jpiString: + case jpiNumeric: + case jpiBool: + /* accessors */ + case jpiKey: + case jpiAnyKey: + /* special items */ + case jpiSubscript: + case jpiLast: + /* item methods */ + case jpiType: + case jpiSize: + case jpiAbs: + case jpiFloor: + case jpiCeiling: + case jpiDouble: + case jpiKeyValue: + status = jpdsNonDateTime; + break; + } + + if (!jspGetNext(jpi, &next)) + break; + + jpi = &next; + } + + return status; +} + +/* + * Check whether jsonpath expression is immutable or not. + */ +bool +jspIsMutable(JsonPath *path, List *varnames, List *varexprs) +{ + JsonPathMutableContext cxt; + JsonPathItem jpi; + + cxt.varnames = varnames; + cxt.varexprs = varexprs; + cxt.current = jpdsNonDateTime; + cxt.lax = (path->header & JSONPATH_LAX) != 0; + cxt.mutable = false; + + jspInit(&jpi, path); + jspIsMutableWalker(&jpi, &cxt); + + return cxt.mutable; +} diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index 2d0599b4aa..eded22e194 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -87,12 +87,16 @@ typedef struct JsonBaseObjectInfo int id; } JsonBaseObjectInfo; +typedef int (*JsonPathVarCallback) (void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); + /* * Context of jsonpath execution. */ typedef struct JsonPathExecContext { - Jsonb *vars; /* variables to substitute into jsonpath */ + void *vars; /* variables to substitute into jsonpath */ + JsonPathVarCallback getVar; JsonbValue *root; /* for $ evaluation */ JsonbValue *current; /* for @ evaluation */ JsonBaseObjectInfo baseObject; /* "base object" for .keyvalue() @@ -174,7 +178,8 @@ typedef JsonPathBool (*JsonPathPredicateCallback) (JsonPathItem *jsp, void *param); typedef Numeric (*BinaryArithmFunc) (Numeric num1, Numeric num2, bool *error); -static JsonPathExecResult executeJsonPath(JsonPath *path, Jsonb *vars, +static JsonPathExecResult executeJsonPath(JsonPath *path, void *vars, + JsonPathVarCallback getVar, Jsonb *json, bool throwErrors, JsonValueList *result, bool useTz); static JsonPathExecResult executeItem(JsonPathExecContext *cxt, @@ -225,8 +230,13 @@ static JsonPathExecResult appendBoolResult(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonValueList *found, JsonPathBool res); static void getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, JsonbValue *value); +static int GetJsonPathVar(void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); static void getJsonPathVariable(JsonPathExecContext *cxt, - JsonPathItem *variable, Jsonb *vars, JsonbValue *value); + JsonPathItem *variable, JsonbValue *value); +static int getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, + int varNameLen, JsonbValue *val, + JsonbValue *baseObject); static int JsonbArraySize(JsonbValue *jb); static JsonPathBool executeComparison(JsonPathItem *cmp, JsonbValue *lv, JsonbValue *rv, void *p); @@ -284,7 +294,8 @@ jsonb_path_exists_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - res = executeJsonPath(jp, vars, jb, !silent, NULL, tz); + res = executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, NULL, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -339,7 +350,8 @@ jsonb_path_match_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -417,7 +429,8 @@ jsonb_path_query_internal(FunctionCallInfo fcinfo, bool tz) vars = PG_GETARG_JSONB_P_COPY(2); silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); funcctx->user_fctx = JsonValueListGetList(&found); @@ -464,7 +477,8 @@ jsonb_path_query_array_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_RETURN_JSONB_P(JsonbValueToJsonb(wrapItemsInArray(&found))); } @@ -495,7 +509,8 @@ jsonb_path_query_first_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); if (JsonValueListLength(&found) >= 1) PG_RETURN_JSONB_P(JsonbValueToJsonb(JsonValueListHead(&found))); @@ -537,8 +552,9 @@ jsonb_path_query_first_tz(PG_FUNCTION_ARGS) * In other case it tries to find all the satisfied result items. */ static JsonPathExecResult -executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, - JsonValueList *result, bool useTz) +executeJsonPath(JsonPath *path, void *vars, JsonPathVarCallback getVar, + Jsonb *json, bool throwErrors, JsonValueList *result, + bool useTz) { JsonPathExecContext cxt; JsonPathExecResult res; @@ -550,22 +566,16 @@ executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, if (!JsonbExtractScalar(&json->root, &jbv)) JsonbInitBinary(&jbv, json); - if (vars && !JsonContainerIsObject(&vars->root)) - { - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"vars\" argument is not an object"), - errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); - } - cxt.vars = vars; + cxt.getVar = getVar; cxt.laxMode = (path->header & JSONPATH_LAX) != 0; cxt.ignoreStructuralErrors = cxt.laxMode; cxt.root = &jbv; cxt.current = &jbv; cxt.baseObject.jbc = NULL; cxt.baseObject.id = 0; - cxt.lastGeneratedObjectId = vars ? 2 : 1; + /* 1 + number of base objects in vars */ + cxt.lastGeneratedObjectId = 1 + getVar(vars, NULL, 0, NULL, NULL); cxt.innermostArraySize = -1; cxt.throwErrors = throwErrors; cxt.useTz = useTz; @@ -2108,54 +2118,118 @@ getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, &value->val.string.len); break; case jpiVariable: - getJsonPathVariable(cxt, item, cxt->vars, value); + getJsonPathVariable(cxt, item, value); return; default: elog(ERROR, "unexpected jsonpath item type"); } } +/* + * Returns the computed value of a JSON path variable with given name. + */ +static int +GetJsonPathVar(void *cxt, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject) +{ + JsonPathVariable *var = NULL; + List *vars = cxt; + ListCell *lc; + int id = 1; + + if (!varName) + return list_length(vars); + + foreach(lc, vars) + { + JsonPathVariable *curvar = lfirst(lc); + + if (!strncmp(curvar->name, varName, varNameLen)) + { + var = curvar; + break; + } + + id++; + } + + if (!var) + return -1; + + if (var->isnull) + { + val->type = jbvNull; + return 0; + } + + JsonItemFromDatum(var->value, var->typid, var->typmod, val); + + *baseObject = *val; + return id; +} + /* * Get the value of variable passed to jsonpath executor */ static void getJsonPathVariable(JsonPathExecContext *cxt, JsonPathItem *variable, - Jsonb *vars, JsonbValue *value) + JsonbValue *value) { char *varName; int varNameLength; + JsonbValue baseObject; + int baseObjectId; + + Assert(variable->type == jpiVariable); + varName = jspGetString(variable, &varNameLength); + + if (!cxt->vars || + (baseObjectId = cxt->getVar(cxt->vars, varName, varNameLength, value, + &baseObject)) < 0) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("could not find jsonpath variable \"%s\"", + pnstrdup(varName, varNameLength)))); + + if (baseObjectId > 0) + setBaseObject(cxt, &baseObject, baseObjectId); +} + +static int +getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, int varNameLength, + JsonbValue *value, JsonbValue *baseObject) +{ + Jsonb *vars = varsJsonb; JsonbValue tmp; JsonbValue *v; - if (!vars) + if (!varName) { - value->type = jbvNull; - return; + if (vars && !JsonContainerIsObject(&vars->root)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"vars\" argument is not an object"), + errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); + } + + return vars ? 1 : 0; /* count of base objects */ } - Assert(variable->type == jpiVariable); - varName = jspGetString(variable, &varNameLength); tmp.type = jbvString; tmp.val.string.val = varName; tmp.val.string.len = varNameLength; v = findJsonbValueFromContainer(&vars->root, JB_FOBJECT, &tmp); - if (v) - { - *value = *v; - pfree(v); - } - else - { - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("could not find jsonpath variable \"%s\"", - pnstrdup(varName, varNameLength)))); - } + if (!v) + return -1; - JsonbInitBinary(&tmp, vars); - setBaseObject(cxt, &tmp, 1); + *value = *v; + pfree(v); + + JsonbInitBinary(baseObject, vars); + return 1; } /**************** Support functions for JsonPath execution *****************/ @@ -2812,3 +2886,240 @@ compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, return DatumGetInt32(DirectFunctionCall2(cmpfunc, val1, val2)); } + +/* Executor-callable JSON_EXISTS implementation */ +bool +JsonPathExists(Datum jb, JsonPath *jp, List *vars, bool *error) +{ + JsonPathExecResult res = executeJsonPath(jp, vars, GetJsonPathVar, + DatumGetJsonbP(jb), !error, NULL, + true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + *error = true; + + return res == jperOk; +} + +/* Executor-callable JSON_QUERY implementation */ +Datum +JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, bool *empty, + bool *error, List *vars) +{ + JsonbValue *first; + bool wrap; + JsonValueList found = {0}; + JsonPathExecResult res PG_USED_FOR_ASSERTS_ONLY; + int count; + + res = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + { + *error = true; + *empty = false; + return (Datum) 0; + } + + count = JsonValueListLength(&found); + + first = count ? JsonValueListHead(&found) : NULL; + + if (!first) + wrap = false; + else if (wrapper == JSW_NONE) + wrap = false; + else if (wrapper == JSW_UNCONDITIONAL) + wrap = true; + else if (wrapper == JSW_CONDITIONAL) + wrap = count > 1 || + IsAJsonbScalar(first) || + (first->type == jbvBinary && + JsonContainerIsScalar(first->val.binary.data)); + else + { + elog(ERROR, "unrecognized json wrapper %d", wrapper); + wrap = false; + } + + if (wrap) + return JsonbPGetDatum(JsonbValueToJsonb(wrapItemsInArray(&found))); + + if (count > 1) + { + if (error) + { + *error = true; + return (Datum) 0; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_QUERY should return singleton item without wrapper"), + errhint("Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array."))); + } + + if (first) + return JsonbPGetDatum(JsonbValueToJsonb(first)); + + *empty = true; + return PointerGetDatum(NULL); +} + +/* Executor-callable JSON_VALUE implementation */ +JsonbValue * +JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars) +{ + JsonbValue *res; + JsonValueList found = {0}; + JsonPathExecResult jper PG_USED_FOR_ASSERTS_ONLY; + int count; + + jper = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(jper)); + + if (error && jperIsError(jper)) + { + *error = true; + *empty = false; + return NULL; + } + + count = JsonValueListLength(&found); + + *empty = !count; + + if (*empty) + return NULL; + + if (count > 1) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + res = JsonValueListHead(&found); + + if (res->type == jbvBinary && + JsonContainerIsScalar(res->val.binary.data)) + JsonbExtractScalar(res->val.binary.data, res); + + if (!IsAJsonbScalar(res)) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_SCALAR_REQUIRED), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + if (res->type == jbvNull) + return NULL; + + return res; +} + +static void +JsonbValueInitNumericDatum(JsonbValue *jbv, Datum num) +{ + jbv->type = jbvNumeric; + jbv->val.numeric = DatumGetNumeric(num); +} + +void +JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) +{ + switch (typid) + { + case BOOLOID: + res->type = jbvBool; + res->val.boolean = DatumGetBool(val); + break; + case NUMERICOID: + JsonbValueInitNumericDatum(res, val); + break; + case INT2OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int2_numeric, val)); + break; + case INT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int4_numeric, val)); + break; + case INT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int8_numeric, val)); + break; + case FLOAT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float4_numeric, val)); + break; + case FLOAT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float8_numeric, val)); + break; + case TEXTOID: + case VARCHAROID: + res->type = jbvString; + res->val.string.val = VARDATA_ANY(val); + res->val.string.len = VARSIZE_ANY_EXHDR(val); + break; + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + res->type = jbvDatetime; + res->val.datetime.value = val; + res->val.datetime.typid = typid; + res->val.datetime.typmod = typmod; + res->val.datetime.tz = 0; + break; + case JSONBOID: + { + JsonbValue *jbv = res; + Jsonb *jb = DatumGetJsonbP(val); + + if (JsonContainerIsScalar(&jb->root)) + { + bool result PG_USED_FOR_ASSERTS_ONLY; + + result = JsonbExtractScalar(&jb->root, jbv); + Assert(result); + } + else + JsonbInitBinary(jbv, jb); + break; + } + case JSONOID: + { + text *txt = DatumGetTextP(val); + char *str = text_to_cstring(txt); + Jsonb *jb; + + jb = DatumGetJsonbP(DirectFunctionCall1(jsonb_in, + CStringGetDatum(str))); + pfree(str); + + JsonItemFromDatum(JsonbPGetDatum(jb), JSONBOID, -1, res); + break; + } + default: + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); + } +} diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d1b03d6cb2..d1ec418ccf 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -476,6 +476,8 @@ static void get_const_expr(Const *constval, deparse_context *context, int showtype); static void get_const_collation(Const *constval, deparse_context *context); static void get_json_format(JsonFormat *format, StringInfo buf); +static void get_json_returning(JsonReturning *returning, StringInfo buf, + bool json_format_by_default); static void get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, bool showimplicit); static void get_json_constructor_options(JsonConstructorExpr *ctor, @@ -518,6 +520,8 @@ static char *generate_qualified_type_name(Oid typid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); +static void get_json_path_spec(Node *path_spec, deparse_context *context, + bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8279,6 +8283,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_WindowFunc: case T_FuncExpr: case T_JsonConstructorExpr: + case T_JsonExpr: /* function-like: name(..) or name[..] */ return true; @@ -8450,6 +8455,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_GroupingFunc: /* own parentheses */ case T_WindowFunc: /* own parentheses */ case T_CaseExpr: /* other separators */ + case T_JsonExpr: /* own parentheses */ return true; default: return false; @@ -8565,6 +8571,65 @@ get_rule_expr_paren(Node *node, deparse_context *context, appendStringInfoChar(context->buf, ')'); } +static void +get_json_behavior(JsonBehavior *behavior, deparse_context *context, + const char *on) +{ + /* + * The order of array elements must correspond to the order of + * JsonBehaviorType members. + */ + const char *behavior_names[] = + { + " NULL", + " ERROR", + " EMPTY", + " TRUE", + " FALSE", + " UNKNOWN", + " EMPTY ARRAY", + " EMPTY OBJECT", + " DEFAULT " + }; + + if ((int) behavior->btype < 0 || behavior->btype >= lengthof(behavior_names)) + elog(ERROR, "invalid json behavior type: %d", behavior->btype); + + appendStringInfoString(context->buf, behavior_names[behavior->btype]); + + if (behavior->btype == JSON_BEHAVIOR_DEFAULT) + get_rule_expr(behavior->default_expr, context, false); + + appendStringInfo(context->buf, " ON %s", on); +} + +/* + * get_json_expr_options + * + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + */ +static void +get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, + JsonBehaviorType default_behavior) +{ + if (jsexpr->op == JSON_QUERY_OP) + { + if (jsexpr->wrapper == JSW_CONDITIONAL) + appendStringInfo(context->buf, " WITH CONDITIONAL WRAPPER"); + else if (jsexpr->wrapper == JSW_UNCONDITIONAL) + appendStringInfo(context->buf, " WITH UNCONDITIONAL WRAPPER"); + + if (jsexpr->omit_quotes) + appendStringInfo(context->buf, " OMIT QUOTES"); + } + + if (jsexpr->op != JSON_EXISTS_OP && + jsexpr->on_empty->btype != default_behavior) + get_json_behavior(jsexpr->on_empty, context, "EMPTY"); + + if (jsexpr->on_error->btype != default_behavior) + get_json_behavior(jsexpr->on_error, context, "ERROR"); +} /* ---------- * get_rule_expr - Parse back an expression @@ -9724,6 +9789,7 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonValueExpr: { JsonValueExpr *jve = (JsonValueExpr *) node; @@ -9773,6 +9839,63 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + appendStringInfoString(buf, "JSON_QUERY("); + break; + case JSON_VALUE_OP: + appendStringInfoString(buf, "JSON_VALUE("); + break; + case JSON_EXISTS_OP: + appendStringInfoString(buf, "JSON_EXISTS("); + break; + } + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_json_path_spec(jexpr->path_spec, context, showimplicit); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoString(buf, " PASSING "); + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + get_rule_expr((Node *) lfirst(lc2), context, showimplicit); + appendStringInfo(buf, " AS %s", + ((String *) lfirst_node(String, lc1))->sval); + } + } + + if (jexpr->op != JSON_EXISTS_OP || + jexpr->returning->typid != BOOLOID) + get_json_returning(jexpr->returning, context->buf, + jexpr->op == JSON_QUERY_OP); + + get_json_expr_options(jexpr, context, + jexpr->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : JSON_BEHAVIOR_NULL); + + appendStringInfoString(buf, ")"); + } + break; + case T_List: { char *sep; @@ -9896,6 +10019,7 @@ looks_like_function(Node *node) case T_MinMaxExpr: case T_SQLValueFunction: case T_XmlExpr: + case T_JsonExpr: /* these are all accepted by func_expr_common_subexpr */ return true; default: @@ -10755,6 +10879,18 @@ get_const_collation(Const *constval, deparse_context *context) } } +/* + * get_json_path_spec - Parse back a JSON path specification + */ +static void +get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit) +{ + if (IsA(path_spec, Const)) + get_const_expr((Const *) path_spec, context, -1); + else + get_rule_expr(path_spec, context, showimplicit); +} + /* * get_json_format - Parse back a JsonFormat node */ diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index 048573c2bc..69fb577850 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -16,12 +16,15 @@ #include "executor/nodeAgg.h" #include "nodes/execnodes.h" +#include "nodes/miscnodes.h" /* forward references to avoid circularity */ struct ExprEvalStep; struct SubscriptingRefState; struct ScalarArrayOpExprHashTable; struct JsonConstructorExprState; +struct JsonbValue; +struct JsonExprState; /* Bits in ExprState->flags (see also execnodes.h for public flag bits): */ /* expression's interpreter has been initialized */ @@ -238,6 +241,11 @@ typedef enum ExprEvalOp EEOP_XMLEXPR, EEOP_JSON_CONSTRUCTOR, EEOP_IS_JSON, + EEOP_JSONEXPR_SKIP, + EEOP_JSONEXPR_PATH, + EEOP_JSONEXPR_BEHAVIOR, + EEOP_JSONEXPR_COERCION, + EEOP_JSONEXPR_COERCION_FINISH, EEOP_AGGREF, EEOP_GROUPING_FUNC, EEOP_WINDOW_FUNC, @@ -689,6 +697,57 @@ typedef struct ExprEvalStep JsonIsPredicate *pred; /* original expression node */ } is_json; + /* for EEOP_JSONEXPR_PATH */ + struct + { + struct JsonExprState *jsestate; + } jsonexpr; + + /* for EEOP_JSONEXPR_SKIP */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprSkip() */ + int jump_coercion; + int jump_passing_args; + } jsonexpr_skip; + + /* for EEOP_JSONEXPR_BEHAVIOR */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprBehavior() */ + int jump_onerror_expr; + int jump_onempty_expr; + int jump_coercion; + int jump_skip_coercion; + } jsonexpr_behavior; + + /* for EEOP_JSONEXPR_COERCION */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion; + + /* for EEOP_JSONEXPR_COERCION_FINISH */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion_finish; } d; } ExprEvalStep; @@ -752,6 +811,111 @@ typedef struct JsonConstructorExprState int nargs; } JsonConstructorExprState; +/* + * Information computed before evaluating a JsonExpr expression. + */ +typedef struct JsonExprPreEvalState +{ + /* value/isnull for JsonExpr.formatted_expr */ + NullableDatum formatted_expr; + + /* value/isnull for JsonExpr.pathspec */ + NullableDatum pathspec; + + /* JsonPathVariable entries for JsonExpr.passing_values */ + List *args; +} JsonExprPreEvalState; + +/* + * State for a given member of JsonItemCoercions. + */ +typedef struct JsonCoercionState +{ + /* Expression used to evaluate the coercion */ + JsonCoercion *coercion; + + /* ExprEvalStep to compute this coercion's expression */ + int jump_eval_expr; +} JsonCoercionState; + +/* + * State for evaluating the coercion for a given JSON item using one of + * the following coercions. + * + * Note that while ExecInitExprRec() for JsonItemCoercions will initialize + * ExprEvalSteps for all of the members that need it, only one will get run + * during a given evaluation of the enclosing JsonExpr depending on the type + * of the result JSON item. + */ +typedef struct JsonItemCoercionsState +{ + JsonCoercionState *null; + JsonCoercionState *string; + JsonCoercionState *numeric; + JsonCoercionState *boolean; + JsonCoercionState *date; + JsonCoercionState *time; + JsonCoercionState *timetz; + JsonCoercionState *timestamp; + JsonCoercionState *timestamptz; + JsonCoercionState *composite; +} JsonItemCoercionsState; + +/* + * State for some post-JsonExpr-evaluation processing steps that gets filled + * in JsonExpr evaluation. + */ +typedef struct JsonExprPostEvalState +{ + /* Is JSON item empty? */ + bool empty; + + /* Did JSON item evaluation cause an error? */ + bool error; + + /* Cache for json_populate_type() called for coercion in some cases */ + void *cache; + + /* + * State for evaluating a JSON item coercion. Points to one of those in + * JsonExprState.coercions chosen by ExecPrepareJsonItemCoercion(). + */ + JsonCoercionState *item_jcstate; + bool coercion_error; + bool coercion_done; + + ErrorSaveContext escontext; +} JsonExprPostEvalState; + +/* EEOP_JSONEXPR state, too big to inline */ +typedef struct JsonExprState +{ + /* original expression node */ + JsonExpr *jsexpr; + + /* + * Should errors be thrown or handled softly? Different parts of + * evaluating a given JsonExpr may require different treatment depending + * on the JsonExprOp; see ExecEvalJsonExprBehavior(). + */ + bool throw_error; + + JsonExprPreEvalState pre_eval; + JsonExprPostEvalState post_eval; + + struct + { + FmgrInfo *finfo; /* typinput function for output type */ + Oid typioparam; + } input; /* I/O info for output type */ + + /* + * Either of the following two is used by ExecEvalJsonExprCoercion() to + * apply coercion to the final result if needed. + */ + JsonCoercionState *result_jcstate; + JsonItemCoercionsState *item_jcstates; +} JsonExprState; /* functions in execExpr.c */ extern void ExprEvalPushStep(ExprState *es, const ExprEvalStep *s); @@ -805,6 +969,14 @@ extern void ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op); extern void ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull); +extern int ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op); +extern void ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, + ExprContext *econtext); extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op); extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op, ExprContext *econtext); diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index ac02247947..089d1c5750 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -17,6 +17,7 @@ #include "executor/execdesc.h" #include "fmgr.h" #include "nodes/lockoptions.h" +#include "nodes/miscnodes.h" #include "nodes/parsenodes.h" #include "utils/memutils.h" diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index cb714f4a19..584bf7001a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -129,6 +129,9 @@ typedef struct ExprState Datum *innermost_domainval; bool *innermost_domainnull; + + /* ErrorSaveContext for soft-error capture. */ + Node *escontext; } ExprState; diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 06d991b725..5e149b0266 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -111,6 +111,7 @@ extern VacuumRelation *makeVacuumRelation(RangeVar *relation, Oid oid, List *va_ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *expr, JsonFormat *format); +extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 06a124a86c..bba2920aba 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1728,6 +1728,12 @@ typedef enum JsonQuotes JS_QUOTES_OMIT /* OMIT QUOTES */ } JsonQuotes; +/* + * JsonPathSpec - + * representation of JSON path constant + */ +typedef char *JsonPathSpec; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1739,6 +1745,48 @@ typedef struct JsonOutput JsonReturning *returning; /* RETURNING FORMAT clause and type Oids */ } JsonOutput; +/* + * JsonArgument - + * representation of argument from JSON PASSING clause + */ +typedef struct JsonArgument +{ + NodeTag type; + JsonValueExpr *val; /* argument value expression */ + char *name; /* argument name */ +} JsonArgument; + +/* + * JsonCommon - + * representation of common syntax of functions using JSON path + */ +typedef struct JsonCommon +{ + NodeTag type; + JsonValueExpr *expr; /* context item expression */ + Node *pathspec; /* JSON path specification expression */ + char *pathname; /* path name, if any */ + List *passing; /* list of PASSING clause arguments, if any */ + int location; /* token location, or -1 if unknown */ +} JsonCommon; + +/* + * JsonFuncExpr - + * untransformed representation of JSON function expressions + */ +typedef struct JsonFuncExpr +{ + NodeTag type; + JsonExprOp op; /* expression type */ + JsonCommon *common; /* common syntax */ + JsonOutput *output; /* output clause, if specified */ + JsonBehavior *on_empty; /* ON EMPTY behavior, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + JsonWrapper wrapper; /* array wrapper behavior (JSON_QUERY only) */ + bool omit_quotes; /* omit or keep quotes? (JSON_QUERY only) */ + int location; /* token location, or -1 if unknown */ +} JsonFuncExpr; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6c6a1810af..0e5a160c90 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1542,6 +1542,17 @@ typedef struct XmlExpr int location; } XmlExpr; +/* + * JsonExprOp - + * enumeration of JSON functions using JSON path + */ +typedef enum JsonExprOp +{ + JSON_VALUE_OP, /* JSON_VALUE() */ + JSON_QUERY_OP, /* JSON_QUERY() */ + JSON_EXISTS_OP /* JSON_EXISTS() */ +} JsonExprOp; + /* * JsonEncoding - * representation of JSON ENCODING clause @@ -1566,6 +1577,37 @@ typedef enum JsonFormatType * jsonb */ } JsonFormatType; +/* + * JsonBehaviorType - + * enumeration of behavior types used in JSON ON ... BEHAVIOR clause + * + * If enum members are reordered, get_json_behavior() from ruleutils.c + * must be updated accordingly. + */ +typedef enum JsonBehaviorType +{ + JSON_BEHAVIOR_NULL = 0, + JSON_BEHAVIOR_ERROR, + JSON_BEHAVIOR_EMPTY, + JSON_BEHAVIOR_TRUE, + JSON_BEHAVIOR_FALSE, + JSON_BEHAVIOR_UNKNOWN, + JSON_BEHAVIOR_EMPTY_ARRAY, + JSON_BEHAVIOR_EMPTY_OBJECT, + JSON_BEHAVIOR_DEFAULT +} JsonBehaviorType; + +/* + * JsonWrapper - + * representation of WRAPPER clause for JSON_QUERY() + */ +typedef enum JsonWrapper +{ + JSW_NONE, + JSW_CONDITIONAL, + JSW_UNCONDITIONAL, +} JsonWrapper; + /* * JsonFormat - * representation of JSON FORMAT clause @@ -1656,6 +1698,73 @@ typedef struct JsonIsPredicate int location; /* token location, or -1 if unknown */ } JsonIsPredicate; +/* + * JsonBehavior - + * representation of JSON ON ... BEHAVIOR clause + */ +typedef struct JsonBehavior +{ + NodeTag type; + JsonBehaviorType btype; /* behavior type */ + Node *default_expr; /* default expression, if any */ +} JsonBehavior; + +/* + * JsonCoercion - + * coercion from SQL/JSON item types to SQL types + */ +typedef struct JsonCoercion +{ + NodeTag type; + Node *expr; /* resulting expression coerced to target type */ + bool via_populate; /* coerce result using json_populate_type()? */ + bool via_io; /* coerce result using type input function? */ + Oid collation; /* collation for coercion via I/O or populate */ +} JsonCoercion; + +/* + * JsonItemCoercions - + * expressions for coercion from SQL/JSON item types directly to the + * output SQL type + */ +typedef struct JsonItemCoercions +{ + NodeTag type; + JsonCoercion *null; + JsonCoercion *string; + JsonCoercion *numeric; + JsonCoercion *boolean; + JsonCoercion *date; + JsonCoercion *time; + JsonCoercion *timetz; + JsonCoercion *timestamp; + JsonCoercion *timestamptz; + JsonCoercion *composite; /* arrays and objects */ +} JsonItemCoercions; + +/* + * JsonExpr - + * transformed representation of JSON_VALUE(), JSON_QUERY(), JSON_EXISTS() + */ +typedef struct JsonExpr +{ + Expr xpr; + JsonExprOp op; /* json function ID */ + Node *formatted_expr; /* formatted context item expression */ + JsonCoercion *result_coercion; /* resulting coercion to RETURNING type */ + JsonFormat *format; /* context item format (JSON/JSONB) */ + Node *path_spec; /* JSON path specification expression */ + List *passing_names; /* PASSING argument names */ + List *passing_values; /* PASSING argument values */ + JsonReturning *returning; /* RETURNING clause type/format info */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + JsonItemCoercions *coercions; /* coercions for JSON_VALUE */ + JsonWrapper wrapper; /* WRAPPER for JSON_QUERY */ + bool omit_quotes; /* KEEP/OMIT QUOTES for JSON_QUERY */ + int location; /* token location, or -1 if unknown */ +} JsonExpr; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 5984dcfa4b..0954d9fc7b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -93,6 +93,7 @@ PG_KEYWORD("commit", COMMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("committed", COMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("compression", COMPRESSION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("concurrently", CONCURRENTLY, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("conditional", CONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("configuration", CONFIGURATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("conflict", CONFLICT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("connection", CONNECTION, UNRESERVED_KEYWORD, BARE_LABEL) @@ -147,11 +148,13 @@ PG_KEYWORD("double", DOUBLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("drop", DROP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("each", EACH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("empty", EMPTY_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("event", EVENT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("except", EXCEPT, RESERVED_KEYWORD, AS_LABEL) @@ -233,10 +236,14 @@ PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_exists", JSON_EXISTS, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) @@ -302,6 +309,7 @@ PG_KEYWORD("off", OFF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("offset", OFFSET, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("oids", OIDS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("old", OLD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("omit", OMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("on", ON, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("only", ONLY, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("operator", OPERATOR, UNRESERVED_KEYWORD, BARE_LABEL) @@ -344,6 +352,7 @@ PG_KEYWORD("procedures", PROCEDURES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("publication", PUBLICATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("quotes", QUOTES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("range", RANGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("read", READ, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("real", REAL, COL_NAME_KEYWORD, BARE_LABEL) @@ -414,6 +423,7 @@ PG_KEYWORD("stdout", STDOUT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("storage", STORAGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("stored", STORED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("string", STRING_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("subscription", SUBSCRIPTION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("substring", SUBSTRING, COL_NAME_KEYWORD, BARE_LABEL) @@ -449,6 +459,7 @@ PG_KEYWORD("types", TYPES_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uescape", UESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unbounded", UNBOUNDED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uncommitted", UNCOMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("unconditional", UNCONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unencrypted", UNENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("union", UNION, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("unique", UNIQUE, RESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/formatting.h b/src/include/utils/formatting.h index 0cad3a2709..d4ca0f42ff 100644 --- a/src/include/utils/formatting.h +++ b/src/include/utils/formatting.h @@ -17,7 +17,6 @@ #ifndef _FORMATTING_H_ #define _FORMATTING_H_ - extern char *str_tolower(const char *buff, size_t nbytes, Oid collid); extern char *str_toupper(const char *buff, size_t nbytes, Oid collid); extern char *str_initcap(const char *buff, size_t nbytes, Oid collid); @@ -29,5 +28,6 @@ extern char *asc_initcap(const char *buff, size_t nbytes); extern Datum parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, Oid *typid, int32 *typmod, int *tz, struct Node *escontext); +extern bool datetime_format_has_tz(const char *fmt_str); #endif diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index 8417a74298..d9e28d14ce 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -439,6 +439,9 @@ extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, int estimated_len); +extern Jsonb *JsonbMakeEmptyArray(void); +extern Jsonb *JsonbMakeEmptyObject(void); +extern char *JsonbUnquote(Jsonb *jb); extern bool JsonbExtractScalar(JsonbContainer *jbc, JsonbValue *res); extern const char *JsonbTypeName(JsonbValue *val); diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index a85203d4a4..cd9cc1480e 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -15,6 +15,7 @@ #define JSONFUNCS_H #include "common/jsonapi.h" +#include "nodes/nodes.h" #include "utils/jsonb.h" /* @@ -63,4 +64,9 @@ extern Jsonb *transform_jsonb_string_values(Jsonb *jsonb, void *action_state, extern text *transform_json_string_values(text *json, void *action_state, JsonTransformStringValuesAction transform_action); +extern Datum json_populate_type(Datum json_val, Oid json_type, + Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext); + #endif diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index f0181e045f..4dae78a98c 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -16,7 +16,9 @@ #include "fmgr.h" #include "nodes/pg_list.h" +#include "nodes/primnodes.h" #include "utils/jsonb.h" +#include "utils/jsonfuncs.h" typedef struct { @@ -184,6 +186,7 @@ extern bool jspGetBool(JsonPathItem *v); extern char *jspGetString(JsonPathItem *v, int32 *len); extern bool jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, int i); +extern bool jspIsMutable(JsonPath *path, List *varnames, List *varexprs); extern const char *jspOperationName(JsonPathItemType type); @@ -261,4 +264,28 @@ extern bool jspConvertRegexFlags(uint32 xflags, int *result, struct Node *escontext); +/* + * Evaluation of jsonpath + */ + +/* External variable passed into jsonpath. */ +typedef struct JsonPathVariable +{ + char *name; + Oid typid; + int32 typmod; + Datum value; + bool isnull; +} JsonPathVariable; + +/* SQL/JSON item */ +extern void JsonItemFromDatum(Datum val, Oid typid, int32 typmod, + JsonbValue *res); + +extern bool JsonPathExists(Datum jb, JsonPath *path, List *vars, bool *error); +extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, + bool *empty, bool *error, List *vars); +extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, + bool *error, List *vars); + #endif diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer index 435c139ec2..b2aa44f36d 100644 --- a/src/interfaces/ecpg/preproc/ecpg.trailer +++ b/src/interfaces/ecpg/preproc/ecpg.trailer @@ -651,6 +651,34 @@ var_type: simple_type $$.type_index = mm_strdup("-1"); $$.type_sizeof = NULL; } + | STRING_P + { + if (INFORMIX_MODE) + { + /* In Informix mode, "string" is automatically a typedef */ + $$.type_enum = ECPGt_string; + $$.type_str = mm_strdup("char"); + $$.type_dimension = mm_strdup("-1"); + $$.type_index = mm_strdup("-1"); + $$.type_sizeof = NULL; + } + else + { + /* Otherwise, legal only if user typedef'ed it */ + struct typedefs *this = get_typedef("string", false); + + $$.type_str = (this->type->type_enum == ECPGt_varchar || this->type->type_enum == ECPGt_bytea) ? EMPTY : mm_strdup(this->name); + $$.type_enum = this->type->type_enum; + $$.type_dimension = this->type->type_dimension; + $$.type_index = this->type->type_index; + if (this->type->type_sizeof && strlen(this->type->type_sizeof) != 0) + $$.type_sizeof = this->type->type_sizeof; + else + $$.type_sizeof = cat_str(3, mm_strdup("sizeof("), mm_strdup(this->name), mm_strdup(")")); + + struct_member_list[struct_level] = ECPGstruct_member_dup(this->struct_member_list); + } + } | INTERVAL ecpg_interval { $$.type_enum = ECPGt_interval; diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out new file mode 100644 index 0000000000..8b87580752 --- /dev/null +++ b/src/test/regress/expected/json_sqljson.out @@ -0,0 +1,18 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); +ERROR: JSON_EXISTS() is not yet implemented for the json type +LINE 1: SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_VALUE +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); +ERROR: JSON_VALUE() is not yet implemented for the json type +LINE 1: SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_QUERY +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); +ERROR: JSON_QUERY() is not yet implemented for the json type +LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out new file mode 100644 index 0000000000..7eb8faf487 --- /dev/null +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -0,0 +1,1034 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL::jsonb, '$'); + json_exists +------------- + +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb 'null', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_EXISTS(jsonb 'null', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{}', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + json_exists +------------- + f +(1 row) + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + json_exists +------------- + t +(1 row) + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); + json_exists +------------- + 1 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); + json_exists +------------- + 0 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); + json_exists +------------- + true +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +ERROR: cannot cast type boolean to jsonb +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +ERROR: cannot cast type boolean to real +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); + ^ +-- JSON_VALUE +SELECT JSON_VALUE(NULL::jsonb, '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$'); + json_value +------------ + true +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + json_value +------------ + t +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); + json_value +------------ + 123 +(1 row) + +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); +ERROR: SQL/JSON item cannot be cast to target type +SELECT JSON_VALUE(jsonb '1.23', '$'); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "1.23" +SELECT JSON_VALUE(jsonb '"aaa"', '$'); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); + json_value +------------ + aa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "aaa" +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); + json_value +------------ + 111 +(1 row) + +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + ?column? +------------ + 03-01-2017 +(1 row) + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '[]', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '{}', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '1', '$.a'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); + json_value +------------ + error +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 3 +(1 row) + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); + json_value +------------ + 0 +(1 row) + +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: " " +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 5 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 1 +(1 row) + +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + x | y +---+---- + 0 | -2 + 1 | 2 + 2 | -1 +(3 rows) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); + json_value +------------ + (1,2) +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + json_value +------------ + (1,2) +(1 row) + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); + json_value +-------------------------- + Tue Feb 20 18:34:56 2018 +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- JSON_QUERY +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + json_query | json_query | json_query | json_query | json_query +--------------------+--------------------+--------------------+----------------------+---------------------- + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, null, "2"] | [1, null, "2"] | [1, null, "2"] | [[1, null, "2"]] | [[1, null, "2"]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] +(6 rows) + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + unspec | without | with cond | with uncond | with +--------------------+--------------------+---------------------+----------------------+---------------------- + | | | | + | | | | + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, 2, 3] | [1, 2, 3] | [1, 2, 3] | [[1, 2, 3]] | [[1, 2, 3]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] + | | [1, "2", null, [3]] | [1, "2", null, [3]] | [1, "2", null, [3]] +(9 rows) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + json_query +------------ + \x616161 +(1 row) + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTE... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTE... + ^ +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_QUERY should return singleton item without wrapper +HINT: Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array. +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); + json_query +------------ + [1, +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); +ERROR: expected JSON array +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + x | y | list +---+---+-------------- + 0 | 0 | [] + 0 | 1 | [1] + 0 | 2 | [1, 2] + 0 | 3 | [1, 2, 3] + 0 | 4 | [1, 2, 3, 4] + 1 | 0 | [] + 1 | 1 | [1] + 1 | 2 | [1, 2] + 1 | 3 | [1, 2, 3] + 1 | 4 | [1, 2, 3, 4] + 2 | 0 | [] + 2 | 1 | [] + 2 | 2 | [2] + 2 | 3 | [2, 3] + 2 | 4 | [2, 3, 4] + 3 | 0 | [] + 3 | 1 | [] + 3 | 2 | [] + 3 | 3 | [3] + 3 | 4 | [3, 4] + 4 | 0 | [] + 4 | 1 | [] + 4 | 2 | [] + 4 | 3 | [] + 4 | 4 | [4] +(25 rows) + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); + json_query +----------------------------------------------------- + (1,aaa,"[1, ""2"", {}]","{""x"": [1, ""2"", {}]}",) +(1 row) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); + unnest +------------------------ + {"a": 1, "b": ["foo"]} + {"a": 2, "c": {}} + 123 +(3 rows) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); + json_query +-------------- + {1,2,NULL,3} +(1 row) + +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); + json_query +------------ + 1 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- Test constraints +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); +\d test_jsonb_constraints + Table "public.test_jsonb_constraints" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+-------------------------------------------------------------------------------- + js | text | | | + i | integer | | | + x | jsonb | | | JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +Check constraints: + "test_jsonb_constraint1" CHECK (js IS JSON) + "test_jsonb_constraint2" CHECK (JSON_EXISTS(js::jsonb, '$."a"' PASSING i + 5 AS int, i::text AS txt, ARRAY[1, 2, 3] AS arr)) + "test_jsonb_constraint3" CHECK (JSON_VALUE(js::jsonb, '$."a"' RETURNING integer DEFAULT ('12'::text || i)::integer ON EMPTY ERROR ON ERROR) > i) + "test_jsonb_constraint4" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb) + "test_jsonb_constraint5" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C")) + "test_jsonb_constraint6" CHECK (JSON_EXISTS(js::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2) + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + check_clause +-------------------------------------------------------------------------------------------------------------------------- + ((JSON_EXISTS((js)::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2)) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C"))) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb)) + ((JSON_VALUE((js)::jsonb, '$."a"' RETURNING integer DEFAULT (('12'::text || i))::integer ON EMPTY ERROR ON ERROR) > i)) + ((js IS JSON)) + (JSON_EXISTS((js)::jsonb, '$."a"' PASSING (i + 5) AS int, (i)::text AS txt, ARRAY[1, 2, 3] AS arr)) +(6 rows) + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +WHERE constraint_name LIKE 'test_jsonb_constraint%'; +ERROR: syntax error at or near "WHERE" +LINE 1: WHERE constraint_name LIKE 'test_jsonb_constraint%'; + ^ +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint1" +DETAIL: Failing row contains (, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains (1, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('[]'); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ([], null, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ({"b": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint3" +DETAIL: Failing row contains ({"a": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint5" +DETAIL: Failing row contains ({"a": 7}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint4" +DETAIL: Failing row contains ({"a": 10}, 1, [1, 2]). +DROP TABLE test_jsonb_constraints; +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index cf46fa3359..cb3230f4dd 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -103,7 +103,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo # ---------- # Another group of parallel tests (JSON related) # ---------- -test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson +test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson json_sqljson jsonb_sqljson # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql new file mode 100644 index 0000000000..4f30fa46b9 --- /dev/null +++ b/src/test/regress/sql/json_sqljson.sql @@ -0,0 +1,11 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + +-- JSON_QUERY + +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql new file mode 100644 index 0000000000..963129699b --- /dev/null +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -0,0 +1,325 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL::jsonb, '$'); + +SELECT JSON_EXISTS(jsonb '[]', '$'); +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + +SELECT JSON_EXISTS(jsonb '1', '$'); +SELECT JSON_EXISTS(jsonb 'null', '$'); +SELECT JSON_EXISTS(jsonb '[]', '$'); + +SELECT JSON_EXISTS(jsonb '1', '$.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_EXISTS(jsonb 'null', '$.a'); +SELECT JSON_EXISTS(jsonb '[]', '$.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); +SELECT JSON_EXISTS(jsonb '{}', '$.a'); +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); + + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL::jsonb, '$'); + +SELECT JSON_VALUE(jsonb 'null', '$'); +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + +SELECT JSON_VALUE(jsonb 'true', '$'); +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + +SELECT JSON_VALUE(jsonb '123', '$'); +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1.23', '$'); +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '"aaa"', '$'); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); + +SELECT JSON_VALUE(jsonb '[]', '$'); +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '{}', '$'); +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1', '$.a'); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- JSON_QUERY + +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); + +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); + +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); + +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- Test constraints + +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); + +\d test_jsonb_constraints + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; +WHERE constraint_name LIKE 'test_jsonb_constraint%'; + +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +INSERT INTO test_jsonb_constraints VALUES ('[]'); +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); + +DROP TABLE test_jsonb_constraints; + +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 7d60511e9e..336b7faa36 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1254,14 +1254,24 @@ JsonArrayAgg JsonArrayConstructor JsonArrayQueryConstructor JsonBaseObjectInfo +JsonBehavior +JsonBehaviorType +JsonCoercion +JsonCoercionState +JsonCommon JsonConstructorExpr JsonConstructorExprState JsonConstructorType JsonEncoding +JsonExpr +JsonExprOp +JsonExprState JsonFormat JsonFormatType JsonHashEntry JsonIsPredicate +JsonItemCoercions +JsonItemCoercionsState JsonIterateStringValuesAction JsonKeyValue JsonLexContext @@ -1294,6 +1304,10 @@ JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback JsonPathString +JsonPathVarCallback +JsonPathVariable +JsonPathVariableEvalContext +JsonQuotes JsonReturning JsonSemAction JsonSerializeExpr -- 2.35.3
v2-0006-Claim-SQL-standard-compliance-for-SQL-JSON-featur.patch (application/octet-stream, 2.3K) download From 906f5b3881246503e61c922894400fa334b9d4fc Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:16:39 +0900 Subject: [PATCH v2 6/6] Claim SQL standard compliance for SQL/JSON features Discussion: https://postgr.es/m/d03d809c-d0fb-fd6a-1476-d6dc18ec940e@dunslane.net --- src/backend/catalog/sql_features.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt index b33065d7bf..b379c71df9 100644 --- a/src/backend/catalog/sql_features.txt +++ b/src/backend/catalog/sql_features.txt @@ -548,15 +548,15 @@ T811 Basic SQL/JSON constructor functions YES T812 SQL/JSON: JSON_OBJECTAGG YES T813 SQL/JSON: JSON_ARRAYAGG with ORDER BY YES T814 Colon in JSON_OBJECT or JSON_OBJECTAGG YES -T821 Basic SQL/JSON query operators NO +T821 Basic SQL/JSON query operators YES T822 SQL/JSON: IS JSON WITH UNIQUE KEYS predicate YES -T823 SQL/JSON: PASSING clause NO -T824 JSON_TABLE: specific PLAN clause NO -T825 SQL/JSON: ON EMPTY and ON ERROR clauses NO -T826 General value expression in ON ERROR or ON EMPTY clauses NO -T827 JSON_TABLE: sibling NESTED COLUMNS clauses NO -T828 JSON_QUERY NO -T829 JSON_QUERY: array wrapper options NO +T823 SQL/JSON: PASSING clause YES +T824 JSON_TABLE: specific PLAN clause YES +T825 SQL/JSON: ON EMPTY and ON ERROR clauses YES +T826 General value expression in ON ERROR or ON EMPTY clauses YES +T827 JSON_TABLE: sibling NESTED COLUMNS clauses YES +T828 JSON_QUERY YES +T829 JSON_QUERY: array wrapper options YES T830 Enforcing unique keys in SQL/JSON constructor functions YES T831 SQL/JSON path language: strict mode YES T832 SQL/JSON path language: item method YES @@ -565,7 +565,7 @@ T834 SQL/JSON path language: wildcard member accessor YES T835 SQL/JSON path language: filter expressions YES T836 SQL/JSON path language: starts with predicate YES T837 SQL/JSON path language: regex_like predicate YES -T838 JSON_TABLE: PLAN DEFAULT clause NO +T838 JSON_TABLE: PLAN DEFAULT clause YES T839 Formatted cast of datetimes to/from character strings NO T840 Hex integer literals in SQL/JSON path language YES T851 SQL/JSON: optional keywords for default syntax YES -- 2.35.3
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-07 12:28  Alvaro Herrera <alvherre@alvh.no-ip.org>
  parent: Amit Langote <amitlangote09@gmail.com>
  1 sibling, 1 reply; 300+ messages in thread

From: Alvaro Herrera @ 2023-07-07 12:28 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: pgsql-hackers

Looking at 0001 now.

I noticed that it adds JSON, JSON_SCALAR and JSON_SERIALIZE as reserved
keywords to doc/src/sgml/keywords/sql2016-02-reserved.txt; but those
keywords do not appear in the 2016 standard as reserved.  I see that
those keywords appear as reserved in sql2023-02-reserved.txt, so I
suppose you're covered as far as that goes; you don't need to patch
sql2016, and indeed that's the wrong thing to do.

I see that you add json_returning_clause_opt, but we already have
json_output_clause_opt.  Shouldn't these two be one and the same?
I think the new name is more sensible than the old one, since the
governing keyword is RETURNING; I suppose naming it "output" comes from
the fact that the standard calls this <JSON output clause>.

typo "requeted"

I'm not in love with the fact that JSON and JSONB have pretty much
parallel type categorizing functionality. It seems entirely artificial.
Maybe this didn't matter when these were contained inside each .c file
and nobody else had to deal with that, but I think it's not good to make
this an exported concept.  Is it possible to do away with that?  I mean,
reduce both to a single categorization enum, and a single categorization
API.  Here you have to cast the enum value to int in order to make
ExecInitExprRec work, and that seems a bit lame; moreso when the
"is_jsonb" is determined separately (cf. ExecEvalJsonConstructor)

In the 2023 standard, JSON_SCALAR is just

<JSON scalar> ::= JSON_SCALAR <left paren> <value expression> <right paren>

but we seem to have added a <JSON output format> clause to it.  Should
we really?

-- 
Álvaro Herrera        Breisgau, Deutschland  —  https://www.EnterpriseDB.com/
"Entristecido, Wutra                     (canción de Las Barreras)
echa a Freyr a rodar
y a nosotros al mar"





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-10 12:56  Amit Langote <amitlangote09@gmail.com>
  parent: Alvaro Herrera <alvherre@alvh.no-ip.org>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-07-10 12:56 UTC (permalink / raw)
  To: Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: pgsql-hackers

Hi Alvaro,

On Fri, Jul 7, 2023 at 9:28 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
> Looking at 0001 now.

Thanks.

> I noticed that it adds JSON, JSON_SCALAR and JSON_SERIALIZE as reserved
> keywords to doc/src/sgml/keywords/sql2016-02-reserved.txt; but those
> keywords do not appear in the 2016 standard as reserved.  I see that
> those keywords appear as reserved in sql2023-02-reserved.txt, so I
> suppose you're covered as far as that goes; you don't need to patch
> sql2016, and indeed that's the wrong thing to do.

Yeah, fixed that after Peter pointed it out.

> I see that you add json_returning_clause_opt, but we already have
> json_output_clause_opt.  Shouldn't these two be one and the same?
> I think the new name is more sensible than the old one, since the
> governing keyword is RETURNING; I suppose naming it "output" comes from
> the fact that the standard calls this <JSON output clause>.

One difference between the two is that json_output_clause_opt allows
specifying the FORMAT clause in addition to the RETURNING type name,
while json_returning_clause_op only allows specifying the type name.

I'm inclined to keep only json_returning_clause_opt as you suggest and
make parse_expr.c output an error if the FORMAT clause is specified in
JSON() and JSON_SCALAR(), so turning the current syntax error on
specifying RETURNING ... FORMAT for these functions into a parsing
error.   Done that way in the attached updated patch and also updated
the latter patch that adds JSON_EXISTS() and JSON_VALUE() to have
similar behavior.

> typo "requeted"

Fixed.

> I'm not in love with the fact that JSON and JSONB have pretty much
> parallel type categorizing functionality. It seems entirely artificial.
> Maybe this didn't matter when these were contained inside each .c file
> and nobody else had to deal with that, but I think it's not good to make
> this an exported concept.  Is it possible to do away with that?  I mean,
> reduce both to a single categorization enum, and a single categorization
> API.  Here you have to cast the enum value to int in order to make
> ExecInitExprRec work, and that seems a bit lame; moreso when the
> "is_jsonb" is determined separately (cf. ExecEvalJsonConstructor)

OK, I agree that a unified categorizing API might be better.  I'll
look at making this better.  Btw, does src/include/common/jsonapi.h
look like an appropriate place for that?

> In the 2023 standard, JSON_SCALAR is just
>
> <JSON scalar> ::= JSON_SCALAR <left paren> <value expression> <right paren>
>
> but we seem to have added a <JSON output format> clause to it.  Should
> we really?

Hmm, I am not seeing <JSON output format> in the rule for JSON_SCALAR,
which looks like this in the current grammar:

func_expr_common_subexpr:
...
            | JSON_SCALAR '(' a_expr json_returning_clause_opt ')'
                {
                    JsonScalarExpr *n = makeNode(JsonScalarExpr);

                    n->expr = (Expr *) $3;
                    n->output = (JsonOutput *) $4;
                    n->location = @1;
                    $$ = (Node *) n;
                }
...
json_returning_clause_opt:
            RETURNING Typename
                {
                    JsonOutput *n = makeNode(JsonOutput);

                    n->typeName = $2;
                    n->returning = makeNode(JsonReturning);
                    n->returning->format =
                        makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, @2);
                    $$ = (Node *) n;
                }
            | /* EMPTY */                           { $$ = NULL; }
            ;

Per what I wrote above, the grammar for JSON() and JSON_SCALAR() does
not allow specifying the FORMAT clause.  Though considering what you
wrote, the RETURNING clause does appear to be an extension to the
standard's spec.  I can't find any reasoning in the original
discussion as to how that came about, except an email from Andrew [1]
saying that he added it back to the patch.

Here's v3 in the meantime.

--
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

[1] https://www.postgresql.org/message-id/flat/cd0bb935-0158-78a7-08b5-904886deac4b%40postgrespro.ru

Attachments: download all (6 files, .tar.gz)

v3-0006-Claim-SQL-standard-compliance-for-SQL-JSON-featur.patch (application/octet-stream, 2.3K) download From 7a59121f189cd8725b62481fbb571690d4f3afa2 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:16:39 +0900 Subject: [PATCH v3 6/6] Claim SQL standard compliance for SQL/JSON features Discussion: https://postgr.es/m/d03d809c-d0fb-fd6a-1476-d6dc18ec940e@dunslane.net --- src/backend/catalog/sql_features.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt index b33065d7bf..b379c71df9 100644 --- a/src/backend/catalog/sql_features.txt +++ b/src/backend/catalog/sql_features.txt @@ -548,15 +548,15 @@ T811 Basic SQL/JSON constructor functions YES T812 SQL/JSON: JSON_OBJECTAGG YES T813 SQL/JSON: JSON_ARRAYAGG with ORDER BY YES T814 Colon in JSON_OBJECT or JSON_OBJECTAGG YES -T821 Basic SQL/JSON query operators NO +T821 Basic SQL/JSON query operators YES T822 SQL/JSON: IS JSON WITH UNIQUE KEYS predicate YES -T823 SQL/JSON: PASSING clause NO -T824 JSON_TABLE: specific PLAN clause NO -T825 SQL/JSON: ON EMPTY and ON ERROR clauses NO -T826 General value expression in ON ERROR or ON EMPTY clauses NO -T827 JSON_TABLE: sibling NESTED COLUMNS clauses NO -T828 JSON_QUERY NO -T829 JSON_QUERY: array wrapper options NO +T823 SQL/JSON: PASSING clause YES +T824 JSON_TABLE: specific PLAN clause YES +T825 SQL/JSON: ON EMPTY and ON ERROR clauses YES +T826 General value expression in ON ERROR or ON EMPTY clauses YES +T827 JSON_TABLE: sibling NESTED COLUMNS clauses YES +T828 JSON_QUERY YES +T829 JSON_QUERY: array wrapper options YES T830 Enforcing unique keys in SQL/JSON constructor functions YES T831 SQL/JSON path language: strict mode YES T832 SQL/JSON path language: item method YES @@ -565,7 +565,7 @@ T834 SQL/JSON path language: wildcard member accessor YES T835 SQL/JSON path language: filter expressions YES T836 SQL/JSON path language: starts with predicate YES T837 SQL/JSON path language: regex_like predicate YES -T838 JSON_TABLE: PLAN DEFAULT clause NO +T838 JSON_TABLE: PLAN DEFAULT clause YES T839 Formatted cast of datetimes to/from character strings NO T840 Hex integer literals in SQL/JSON path language YES T851 SQL/JSON: optional keywords for default syntax YES -- 2.35.3
v3-0002-Don-t-include-CaseTestExpr-in-JsonValueExpr.forma.patch (application/octet-stream, 2.9K) download From 2f81e3d0b4b95dbf4a7d92cd52f8f682dc2046a1 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Fri, 7 Jul 2023 20:21:58 +0900 Subject: [PATCH v3 2/6] Don't include CaseTestExpr in JsonValueExpr.formatted_expr There doesn't appear to be any need for it. --- src/backend/executor/execExpr.c | 11 ----------- src/backend/optimizer/util/clauses.c | 5 ----- src/backend/parser/parse_expr.c | 7 ++----- 3 files changed, 2 insertions(+), 21 deletions(-) diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index e6e616865c..21f7796e63 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -2297,18 +2297,7 @@ ExecInitExprRec(Expr *node, ExprState *state, ExecInitExprRec(jve->raw_expr, state, resv, resnull); if (jve->formatted_expr) - { - Datum *innermost_caseval = state->innermost_caseval; - bool *innermost_isnull = state->innermost_casenull; - - state->innermost_caseval = resv; - state->innermost_casenull = resnull; - ExecInitExprRec(jve->formatted_expr, state, resv, resnull); - - state->innermost_caseval = innermost_caseval; - state->innermost_casenull = innermost_isnull; - } break; } diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 7f453b04f8..59d8ce789e 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -2834,15 +2834,10 @@ eval_const_expressions_mutator(Node *node, if (raw && IsA(raw, Const)) { Node *formatted; - Node *save_case_val = context->case_val; - - context->case_val = raw; formatted = eval_const_expressions_mutator((Node *) jve->formatted_expr, context); - context->case_val = save_case_val; - if (formatted && IsA(formatted, Const)) return formatted; } diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 9e8a2ac22b..f02343ef64 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -3269,11 +3269,8 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, if (format != JS_FORMAT_DEFAULT) { Oid targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; - Node *orig = makeCaseTestExpr(expr); Node *coerced; - expr = orig; - if (exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), @@ -3311,7 +3308,7 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, coerced = (Node *) fexpr; } - if (coerced == orig) + if (coerced == expr) expr = rawexpr; else { @@ -3899,7 +3896,7 @@ transformJsonParseArg(ParseState *pstate, Node *jsexpr, JsonFormat *format, { JsonValueExpr *jve; - expr = makeCaseTestExpr(raw_expr); + expr = raw_expr; expr = makeJsonByteaToTextConversion(expr, format, exprLocation(expr)); *exprtype = TEXTOID; -- 2.35.3
v3-0003-SQL-JSON-functions.patch (application/octet-stream, 59.8K) download From 54516f4c8eaa327dad273df3106fab2df1106341 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:12:39 +0900 Subject: [PATCH v3 3/6] SQL JSON functions This Patch introduces three SQL standard JSON functions: JSON() JSON_SCALAR() JSON_SERIALIZE() JSON() produces json values from text, bytea, json or jsonb values, and has facilitites for handling duplicate keys. JSON_SCALAR() produces a json value from any scalar sql value, including json and jsonb. JSON_SERIALIZE() produces text or bytea from input which containis or represents json or jsonb; For the most part these functions don't add any significant new capabilities, but they will be of use to users wanting standard compliant JSON handling. Nikita Glukhov Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby. Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org --- doc/src/sgml/func.sgml | 71 +++++ src/backend/executor/execExpr.c | 45 +++ src/backend/executor/execExprInterp.c | 42 ++- src/backend/nodes/nodeFuncs.c | 30 ++ src/backend/parser/gram.y | 69 ++++- src/backend/parser/parse_expr.c | 231 +++++++++++++++- src/backend/parser/parse_target.c | 9 + src/backend/utils/adt/format_type.c | 4 + src/backend/utils/adt/json.c | 44 ++- src/backend/utils/adt/jsonb.c | 76 ++--- src/backend/utils/adt/ruleutils.c | 14 +- src/include/nodes/parsenodes.h | 48 ++++ src/include/nodes/primnodes.h | 5 +- src/include/parser/kwlist.h | 4 +- src/include/utils/json.h | 19 ++ src/include/utils/jsonb.h | 21 ++ src/test/regress/expected/sqljson.out | 384 ++++++++++++++++++++++++++ src/test/regress/sql/sqljson.sql | 95 +++++++ src/tools/pgindent/typedefs.list | 1 + 19 files changed, 1123 insertions(+), 89 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 0b62e0c828..9cece06c18 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16001,6 +16001,77 @@ table2-mapping <returnvalue>{"a": "1", "b": "2"}</returnvalue> </para></entry> </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json constructor</primary></indexterm> + <function>json</function> ( + <replaceable>expression</replaceable> + <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional></optional> + <optional> { <literal>WITH</literal> | <literal>WITHOUT</literal> } <literal>UNIQUE</literal> <optional> <literal>KEYS</literal> </optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>json_data_type</replaceable> </optional>) + </para> + <para> + The <replaceable>expression</replaceable> can be any text type or a + <type>bytea</type> in UTF8 encoding. If the + <replaceable>expression</replaceable> is NULL, an + <acronym>SQL</acronym> null value is returned. + If <literal>WITH UNIQUE</literal> is specified, the + <replaceable>expression</replaceable> must not contain any duplicate + object keys. + </para> + <para> + <literal>json('{"a":123, "b":[true,"foo"], "a":"bar"}')</literal> + <returnvalue>{"a":123, "b":[true,"foo"], "a":"bar"}</returnvalue> + </para> + <para> + <literal>json('{"a":123,"b":[true,"foo"],"a":"bar"}' returning jsonb)</literal> + <returnvalue>{"a": "bar", "b": [true, "foo"]}</returnvalue> + </para> + </entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json_scalar</primary></indexterm> + <function>json_scalar</function> (<replaceable>expression</replaceable>) + <optional> <literal>RETURNING</literal> <replaceable>json_data_type</replaceable> </optional>) + </para> + <para> + Returns a JSON scalar value representing + <replaceable>expression</replaceable>. + If the input is NULL, an SQL NULL is returned. If the input is a number + or a boolean value, a corresponding JSON number or boolean value is + returned. For any other value a JSON string is returned. + </para> + <para> + <literal>json_scalar(123.45)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_scalar(CURRENT_TIMESTAMP)</literal> + <returnvalue>"2022-05-10T10:51:04.62128-04:00"</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <function>json_serialize</function> ( + <replaceable>expression</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional>) + </para> + <para> + Transforms an SQL/JSON value into a character or binary string. The + <replaceable>expression</replaceable> can be of any JSON type, any + character string type, or <type>bytea</type> in UTF8 encoding. + The returned type can be any character string type or + <type>bytea</type>. The default is <type>text</type>. + </para> + <para> + <literal>json_serialize('{ "a" : 1 } ' RETURNING bytea)</literal> + <returnvalue>\x7b20226122203a2031207d20</returnvalue> + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 21f7796e63..95d9fbbc7c 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -48,6 +48,8 @@ #include "utils/array.h" #include "utils/builtins.h" #include "utils/datum.h" +#include "utils/json.h" +#include "utils/jsonb.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -2313,6 +2315,12 @@ ExecInitExprRec(Expr *node, ExprState *state, { ExecInitExprRec(ctor->func, state, resv, resnull); } + else if ((ctor->type == JSCTOR_JSON_PARSE && !ctor->unique) || + ctor->type == JSCTOR_JSON_SERIALIZE) + { + /* Use the value of the first argument as a result */ + ExecInitExprRec(linitial(args), state, resv, resnull); + } else { JsonConstructorExprState *jcstate; @@ -2351,6 +2359,43 @@ ExecInitExprRec(Expr *node, ExprState *state, argno++; } + /* prepare type cache for datum_to_json[b]() */ + if (ctor->type == JSCTOR_JSON_SCALAR) + { + bool is_jsonb = + ctor->returning->format->format_type == JS_FORMAT_JSONB; + + jcstate->arg_type_cache = + palloc(sizeof(*jcstate->arg_type_cache) * nargs); + + for (int i = 0; i < nargs; i++) + { + int category; + Oid outfuncid; + Oid typid = jcstate->arg_types[i]; + + if (is_jsonb) + { + JsonbTypeCategory jbcat; + + jsonb_categorize_type(typid, &jbcat, &outfuncid); + + category = (int) jbcat; + } + else + { + JsonTypeCategory jscat; + + json_categorize_type(typid, &jscat, &outfuncid); + + category = (int) jscat; + } + + jcstate->arg_type_cache[i].outfuncid = outfuncid; + jcstate->arg_type_cache[i].category = category; + } + } + ExprEvalPushStep(state, &scratch); } diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 851946a927..b83dd1c666 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -3992,7 +3992,7 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_values, jcstate->arg_nulls, jcstate->arg_types, - jcstate->constructor->absent_on_null); + ctor->absent_on_null); else if (ctor->type == JSCTOR_JSON_OBJECT) res = (is_jsonb ? jsonb_build_object_worker : @@ -4002,6 +4002,46 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_types, jcstate->constructor->absent_on_null, jcstate->constructor->unique); + else if (ctor->type == JSCTOR_JSON_SCALAR) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + int category = jcstate->arg_type_cache[0].category; + Oid outfuncid = jcstate->arg_type_cache[0].outfuncid; + + if (is_jsonb) + res = to_jsonb_worker(value, category, outfuncid); + else + res = to_json_worker(value, category, outfuncid); + } + } + else if (ctor->type == JSCTOR_JSON_PARSE) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + text *js = DatumGetTextP(value); + + if (is_jsonb) + res = jsonb_from_text(js, true); + else + { + (void) json_validate(js, true, true); + res = value; + } + } + } else elog(ERROR, "invalid JsonConstructorExpr type %d", ctor->type); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index c41e6bb984..dda964bd19 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -3901,6 +3901,36 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonParseExpr: + { + JsonParseExpr *jpe = (JsonParseExpr *) node; + + if (WALK(jpe->expr)) + return true; + if (WALK(jpe->output)) + return true; + } + break; + case T_JsonScalarExpr: + { + JsonScalarExpr *jse = (JsonScalarExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; + case T_JsonSerializeExpr: + { + JsonSerializeExpr *jse = (JsonSerializeExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; case T_JsonConstructorExpr: { JsonConstructorExpr *ctor = (JsonConstructorExpr *) node; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index edb6c00ece..341b002dc7 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -566,7 +566,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <list> copy_options %type <typnam> Typename SimpleTypename ConstTypename - GenericType Numeric opt_float + GenericType Numeric opt_float JsonType Character ConstCharacter CharacterWithLength CharacterWithoutLength ConstDatetime ConstInterval @@ -647,7 +647,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> json_format_clause_opt json_value_expr - json_output_clause_opt + json_returning_clause_opt json_name_and_value json_aggregate_func %type <list> json_name_and_value_list @@ -659,7 +659,6 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt - /* * Non-keyword token types. These are hard-wired into the "flex" lexer. * They must be listed first so that their numeric codes do not depend on @@ -723,6 +722,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG + JSON_SCALAR JSON_SERIALIZE KEY KEYS @@ -13981,6 +13981,7 @@ SimpleTypename: $$->typmods = list_make2(makeIntConst(INTERVAL_FULL_RANGE, -1), makeIntConst($3, @3)); } + | JsonType { $$ = $1; } ; /* We have a separate ConstTypename to allow defaulting fixed-length @@ -13999,6 +14000,7 @@ ConstTypename: | ConstBit { $$ = $1; } | ConstCharacter { $$ = $1; } | ConstDatetime { $$ = $1; } + | JsonType { $$ = $1; } ; /* @@ -14367,6 +14369,13 @@ interval_second: } ; +JsonType: + JSON + { + $$ = SystemTypeName("json"); + $$->location = @1; + } + ; /***************************************************************************** * @@ -15561,7 +15570,7 @@ func_expr_common_subexpr: | JSON_OBJECT '(' json_name_and_value_list json_object_constructor_null_clause_opt json_key_uniqueness_constraint_opt - json_output_clause_opt ')' + json_returning_clause_opt ')' { JsonObjectConstructor *n = makeNode(JsonObjectConstructor); @@ -15572,7 +15581,7 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } - | JSON_OBJECT '(' json_output_clause_opt ')' + | JSON_OBJECT '(' json_returning_clause_opt ')' { JsonObjectConstructor *n = makeNode(JsonObjectConstructor); @@ -15586,7 +15595,7 @@ func_expr_common_subexpr: | JSON_ARRAY '(' json_value_expr_list json_array_constructor_null_clause_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayConstructor *n = makeNode(JsonArrayConstructor); @@ -15601,7 +15610,7 @@ func_expr_common_subexpr: select_no_parens json_format_clause_opt /* json_array_constructor_null_clause_opt */ - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayQueryConstructor *n = makeNode(JsonArrayQueryConstructor); @@ -15614,7 +15623,7 @@ func_expr_common_subexpr: $$ = (Node *) n; } | JSON_ARRAY '(' - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayConstructor *n = makeNode(JsonArrayConstructor); @@ -15625,7 +15634,37 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } - ; + | JSON '(' json_value_expr json_key_uniqueness_constraint_opt + json_returning_clause_opt ')' + { + JsonParseExpr *n = makeNode(JsonParseExpr); + + n->expr = (JsonValueExpr *) $3; + n->unique_keys = $4; + n->output = (JsonOutput *) $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SCALAR '(' a_expr json_returning_clause_opt ')' + { + JsonScalarExpr *n = makeNode(JsonScalarExpr); + + n->expr = (Expr *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SERIALIZE '(' json_value_expr json_returning_clause_opt ')' + { + JsonSerializeExpr *n = makeNode(JsonSerializeExpr); + + n->expr = (JsonValueExpr *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + ; + /* * SQL/XML support @@ -16373,7 +16412,7 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; -json_output_clause_opt: +json_returning_clause_opt: RETURNING Typename json_format_clause_opt { JsonOutput *n = makeNode(JsonOutput); @@ -16446,7 +16485,7 @@ json_aggregate_func: json_name_and_value json_object_constructor_null_clause_opt json_key_uniqueness_constraint_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonObjectAgg *n = makeNode(JsonObjectAgg); @@ -16464,7 +16503,7 @@ json_aggregate_func: json_value_expr json_array_aggregate_order_by_clause_opt json_array_constructor_null_clause_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayAgg *n = makeNode(JsonArrayAgg); @@ -17064,7 +17103,6 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION - | JSON | KEY | KEYS | LABEL @@ -17279,10 +17317,13 @@ col_name_keyword: | INT_P | INTEGER | INTERVAL + | JSON | JSON_ARRAY | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | LEAST | NATIONAL | NCHAR @@ -17643,6 +17684,8 @@ bare_label_keyword: | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | KEY | KEYS | LABEL diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index f02343ef64..e978dc3e76 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -86,6 +86,10 @@ static Node *transformJsonArrayQueryConstructor(ParseState *pstate, static Node *transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg); static Node *transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg); static Node *transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred); +static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); +static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); +static Node *transformJsonSerializeExpr(ParseState *pstate, + JsonSerializeExpr * expr); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -337,6 +341,18 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonIsPredicate(pstate, (JsonIsPredicate *) expr); break; + case T_JsonParseExpr: + result = transformJsonParseExpr(pstate, (JsonParseExpr *) expr); + break; + + case T_JsonScalarExpr: + result = transformJsonScalarExpr(pstate, (JsonScalarExpr *) expr); + break; + + case T_JsonSerializeExpr: + result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3224,7 +3240,8 @@ makeCaseTestExpr(Node *expr) static Node * transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, char *constructName, - JsonFormatType default_format) + JsonFormatType default_format, + Oid targettype) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3266,12 +3283,14 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, else format = default_format; - if (format != JS_FORMAT_DEFAULT) + if (format != JS_FORMAT_DEFAULT || + (OidIsValid(targettype) && exprtype != targettype)) { - Oid targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; Node *coerced; + bool cast_is_needed = OidIsValid(targettype); - if (exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) + if (!cast_is_needed && + exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), errmsg(ve->format->format_type == JS_FORMAT_DEFAULT ? @@ -3287,6 +3306,9 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, exprtype = TEXTOID; } + if (!OidIsValid(targettype)) + targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; + /* Try to coerce to the target type. */ coerced = coerce_to_target_type(pstate, expr, exprtype, targettype, -1, @@ -3297,11 +3319,20 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, if (!coerced) { /* If coercion failed, use to_json()/to_jsonb() functions. */ - Oid fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; - FuncExpr *fexpr = makeFuncExpr(fnoid, targettype, - list_make1(expr), - InvalidOid, InvalidOid, - COERCE_EXPLICIT_CALL); + FuncExpr *fexpr; + Oid fnoid; + + if (cast_is_needed) /* only CAST is allowed */ + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(exprtype), + format_type_be(targettype)), + parser_errposition(pstate, location))); + + fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; + fexpr = makeFuncExpr(fnoid, targettype, list_make1(expr), + InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL); fexpr->location = location; @@ -3584,7 +3615,8 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) Node *key = transformExprRecurse(pstate, (Node *) kv->key); Node *val = transformJsonValueExpr(pstate, kv->value, "JSON_OBJECT()", - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, key); args = lappend(args, val); @@ -3764,7 +3796,7 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) key = transformExprRecurse(pstate, (Node *) agg->arg->key); val = transformJsonValueExpr(pstate, agg->arg->value, "JSON_OBJECTAGG()", - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, InvalidOid); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3821,7 +3853,7 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) Oid aggtype; arg = transformJsonValueExpr(pstate, agg->arg, "JSON_ARRAYAGG()", - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, InvalidOid); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3869,7 +3901,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, jsval, "JSON_ARRAY()", - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, val); } @@ -3952,3 +3985,175 @@ transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred) return makeJsonIsPredicate(expr, NULL, pred->item_type, pred->unique_keys, pred->location); } + +/* + * Transform the output clause of a JSON_*() expression if there is one and + * create one if not. + */ +static JsonReturning * +transformJsonReturning(ParseState *pstate, JsonOutput *output, const char *fname) +{ + JsonReturning *returning; + + if (output) + { + returning = transformJsonOutput(pstate, output, false); + + Assert(OidIsValid(returning->typid)); + + if (returning->typid != JSONOID && returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), fname), + parser_errposition(pstate, output->typeName->location))); + } + else + { + /* Output type is JSON by default. */ + Oid targettype = JSONOID; + JsonFormatType format = JS_FORMAT_JSON; + + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(format, JS_ENC_DEFAULT, -1); + returning->typid = targettype; + returning->typmod = -1; + } + + return returning; +} + +/* + * Transform a JSON() expression. + */ +static Node * +transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) +{ + JsonOutput *output = jsexpr->output; + JsonReturning *returning; + Node *arg; + + /* Disallow FORMAT specification in the RETURNING clause. */ + if (output) + { + JsonFormat *format = output->returning->format; + + if(format->format_type != JS_FORMAT_DEFAULT || + format->encoding != JS_ENC_DEFAULT) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot specify FORMAT in RETURNING clause of JSON()"), + parser_errposition(pstate, format->location))); + } + + returning = transformJsonReturning(pstate, output, "JSON()"); + + if (jsexpr->unique_keys) + { + /* + * Coerce string argument to text and then to json[b] in the executor + * node with key uniqueness check. + */ + JsonValueExpr *jve = jsexpr->expr; + Oid arg_type; + + arg = transformJsonParseArg(pstate, (Node *) jve->raw_expr, jve->format, + &arg_type); + + if (arg_type != TEXTOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use non-string types with WITH UNIQUE KEYS clause"), + parser_errposition(pstate, jsexpr->location))); + } + else + { + /* + * Coerce argument to target type using CAST for compatibility with PG + * function-like CASTs. + */ + arg = transformJsonValueExpr(pstate, jsexpr->expr, "JSON()", + JS_FORMAT_JSON, returning->typid); + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, + returning, jsexpr->unique_keys, false, + jsexpr->location); +} + +/* + * Transform a JSON_SCALAR() expression. + */ +static Node * +transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *jsexpr) +{ + Node *arg = transformExprRecurse(pstate, (Node *) jsexpr->expr); + JsonOutput *output = jsexpr->output; + JsonReturning *returning; + + /* Disallow FORMAT specification in the RETURNING clause. */ + if (output) + { + JsonFormat *format = output->returning->format; + + if(format->format_type != JS_FORMAT_DEFAULT || + format->encoding != JS_ENC_DEFAULT) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot specify FORMAT in RETURNING clause of JSON_SCALAR()"), + parser_errposition(pstate, format->location))); + } + + returning = transformJsonReturning(pstate, output, "JSON_SCALAR()"); + + if (exprType(arg) == UNKNOWNOID) + arg = coerce_to_specific_type(pstate, arg, TEXTOID, "JSON_SCALAR"); + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SCALAR, list_make1(arg), NULL, + returning, false, false, jsexpr->location); +} + +/* + * Transform a JSON_SERIALIZE() expression. + */ +static Node * +transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) +{ + Node *arg = transformJsonValueExpr(pstate, expr->expr, + "JSON_SERIALIZE()", + JS_FORMAT_JSON, + InvalidOid); + JsonReturning *returning; + + if (expr->output) + { + returning = transformJsonOutput(pstate, expr->output, true); + + if (returning->typid != BYTEAOID) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(returning->typid, &typcategory, + &typispreferred); + if (typcategory != TYPCATEGORY_STRING) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), + "JSON_SERIALIZE()"), + errhint("Try returning a string type or bytea."))); + } + } + else + { + /* RETURNING TEXT FORMAT JSON is by default */ + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(JS_FORMAT_JSON, JS_ENC_DEFAULT, -1); + returning->typid = TEXTOID; + returning->typmod = -1; + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), + NULL, returning, false, false, expr->location); +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4cca97ff9c..520d4f2a23 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1953,6 +1953,15 @@ FigureColnameInternal(Node *node, char **name) /* make XMLSERIALIZE act like a regular function */ *name = "xmlserialize"; return 2; + case T_JsonParseExpr: + *name = "json"; + return 2; + case T_JsonScalarExpr: + *name = "json_scalar"; + return 2; + case T_JsonSerializeExpr: + *name = "json_serialize"; + return 2; case T_JsonObjectConstructor: /* make JSON_OBJECT act like a regular function */ *name = "json_object"; diff --git a/src/backend/utils/adt/format_type.c b/src/backend/utils/adt/format_type.c index 12402a0637..36c45a3978 100644 --- a/src/backend/utils/adt/format_type.c +++ b/src/backend/utils/adt/format_type.c @@ -294,6 +294,10 @@ format_type_extended(Oid type_oid, int32 typemod, bits16 flags) else buf = pstrdup("character varying"); break; + + case JSONOID: + buf = pstrdup("json"); + break; } if (buf == NULL) diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index 49080e5fbf..6d8fcdf40d 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -29,21 +29,6 @@ #include "utils/lsyscache.h" #include "utils/typcache.h" -typedef enum /* type categories for datum_to_json */ -{ - JSONTYPE_NULL, /* null, so we didn't bother to identify */ - JSONTYPE_BOOL, /* boolean (built-in types only) */ - JSONTYPE_NUMERIC, /* numeric (ditto) */ - JSONTYPE_DATE, /* we use special formatting for datetimes */ - JSONTYPE_TIMESTAMP, - JSONTYPE_TIMESTAMPTZ, - JSONTYPE_JSON, /* JSON itself (and JSONB) */ - JSONTYPE_ARRAY, /* array */ - JSONTYPE_COMPOSITE, /* composite */ - JSONTYPE_CAST, /* something with an explicit cast to JSON */ - JSONTYPE_OTHER /* all else */ -} JsonTypeCategory; - /* * Support for fast key uniqueness checking. @@ -107,9 +92,6 @@ static void array_dim_to_json(StringInfo result, int dim, int ndims, int *dims, bool use_line_feeds); static void array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds); -static void json_categorize_type(Oid typoid, - JsonTypeCategory *tcategory, - Oid *outfuncoid); static void datum_to_json(Datum val, bool is_null, StringInfo result, JsonTypeCategory tcategory, Oid outfuncoid, bool key_scalar); @@ -188,8 +170,11 @@ json_recv(PG_FUNCTION_ARGS) * Given the datatype OID, return its JsonTypeCategory, as well as the type's * output function OID. If the returned category is JSONTYPE_CAST, we * return the OID of the type->JSON cast function instead. + * + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. */ -static void +void json_categorize_type(Oid typoid, JsonTypeCategory *tcategory, Oid *outfuncoid) @@ -771,6 +756,20 @@ row_to_json_pretty(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); } +/* + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. + */ +Datum +to_json_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid) +{ + StringInfo result = makeStringInfo(); + + datum_to_json(val, false, result, tcategory, outfuncoid, false); + + return PointerGetDatum(cstring_to_text_with_len(result->data, result->len)); +} + /* * Is the given type immutable when coming out of a JSON context? * @@ -821,7 +820,6 @@ to_json(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - StringInfo result; JsonTypeCategory tcategory; Oid outfuncoid; @@ -833,11 +831,7 @@ to_json(PG_FUNCTION_ARGS) json_categorize_type(val_type, &tcategory, &outfuncoid); - result = makeStringInfo(); - - datum_to_json(val, false, result, tcategory, outfuncoid, false); - - PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); + PG_RETURN_DATUM(to_json_worker(val, tcategory, outfuncoid)); } /* diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index cf43c3f2de..7a91177a55 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -34,26 +34,10 @@ typedef struct JsonbInState { JsonbParseState *parseState; JsonbValue *res; + bool unique_keys; Node *escontext; } JsonbInState; -/* unlike with json categories, we need to treat json and jsonb differently */ -typedef enum /* type categories for datum_to_jsonb */ -{ - JSONBTYPE_NULL, /* null, so we didn't bother to identify */ - JSONBTYPE_BOOL, /* boolean (built-in types only) */ - JSONBTYPE_NUMERIC, /* numeric (ditto) */ - JSONBTYPE_DATE, /* we use special formatting for datetimes */ - JSONBTYPE_TIMESTAMP, /* we use special formatting for timestamp */ - JSONBTYPE_TIMESTAMPTZ, /* ... and timestamptz */ - JSONBTYPE_JSON, /* JSON */ - JSONBTYPE_JSONB, /* JSONB */ - JSONBTYPE_ARRAY, /* array */ - JSONBTYPE_COMPOSITE, /* composite */ - JSONBTYPE_JSONCAST, /* something with an explicit cast to JSON */ - JSONBTYPE_OTHER /* all else */ -} JsonbTypeCategory; - typedef struct JsonbAggState { JsonbInState *res; @@ -63,7 +47,8 @@ typedef struct JsonbAggState Oid val_output_func; } JsonbAggState; -static inline Datum jsonb_from_cstring(char *json, int len, Node *escontext); +static inline Datum jsonb_from_cstring(char *json, int len, bool unique_keys, + Node *escontext); static bool checkStringLen(size_t len, Node *escontext); static JsonParseErrorType jsonb_in_object_start(void *pstate); static JsonParseErrorType jsonb_in_object_end(void *pstate); @@ -72,17 +57,11 @@ static JsonParseErrorType jsonb_in_array_end(void *pstate); static JsonParseErrorType jsonb_in_object_field_start(void *pstate, char *fname, bool isnull); static void jsonb_put_escaped_value(StringInfo out, JsonbValue *scalarVal); static JsonParseErrorType jsonb_in_scalar(void *pstate, char *token, JsonTokenType tokentype); -static void jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid); static void composite_to_jsonb(Datum composite, JsonbInState *result); static void array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *dims, Datum *vals, bool *nulls, int *valcount, JsonbTypeCategory tcategory, Oid outfuncoid); static void array_to_jsonb_internal(Datum array, JsonbInState *result); -static void jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid); static void datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, JsonbTypeCategory tcategory, Oid outfuncoid, bool key_scalar); @@ -100,7 +79,7 @@ jsonb_in(PG_FUNCTION_ARGS) { char *json = PG_GETARG_CSTRING(0); - return jsonb_from_cstring(json, strlen(json), fcinfo->context); + return jsonb_from_cstring(json, strlen(json), false, fcinfo->context); } /* @@ -124,7 +103,7 @@ jsonb_recv(PG_FUNCTION_ARGS) else elog(ERROR, "unsupported jsonb version number %d", version); - return jsonb_from_cstring(str, nbytes, NULL); + return jsonb_from_cstring(str, nbytes, false, NULL); } /* @@ -165,6 +144,18 @@ jsonb_send(PG_FUNCTION_ARGS) PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); } +/* + * Note: exported for use in SQL/JSON executor functions. + */ +Datum +jsonb_from_text(text *js, bool unique_keys) +{ + return jsonb_from_cstring(VARDATA_ANY(js), + VARSIZE_ANY_EXHDR(js), + unique_keys, + NULL); +} + /* * Get the type name of a jsonb container. */ @@ -258,7 +249,7 @@ jsonb_typeof(PG_FUNCTION_ARGS) * instead of being thrown. */ static inline Datum -jsonb_from_cstring(char *json, int len, Node *escontext) +jsonb_from_cstring(char *json, int len, bool unique_keys, Node *escontext) { JsonLexContext *lex; JsonbInState state; @@ -268,6 +259,7 @@ jsonb_from_cstring(char *json, int len, Node *escontext) memset(&sem, 0, sizeof(sem)); lex = makeJsonLexContextCstringLen(json, len, GetDatabaseEncoding(), true); + state.unique_keys = unique_keys; state.escontext = escontext; sem.semstate = (void *) &state; @@ -304,6 +296,7 @@ jsonb_in_object_start(void *pstate) JsonbInState *_state = (JsonbInState *) pstate; _state->res = pushJsonbValue(&_state->parseState, WJB_BEGIN_OBJECT, NULL); + _state->parseState->unique_keys = _state->unique_keys; return JSON_SUCCESS; } @@ -639,8 +632,11 @@ add_indent(StringInfo out, bool indent, int level) * Given the datatype OID, return its JsonbTypeCategory, as well as the type's * output function OID. If the returned category is JSONBTYPE_JSONCAST, * we return the OID of the relevant cast function instead. + * + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. */ -static void +void jsonb_categorize_type(Oid typoid, JsonbTypeCategory *tcategory, Oid *outfuncoid) @@ -1150,6 +1146,23 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, datum_to_jsonb(val, is_null, result, tcategory, outfuncoid, key_scalar); } + +/* + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. + */ +Datum +to_jsonb_worker(Datum val, JsonbTypeCategory tcategory, Oid outfuncoid) +{ + JsonbInState result; + + memset(&result, 0, sizeof(JsonbInState)); + + datum_to_jsonb(val, false, &result, tcategory, outfuncoid, false); + + return JsonbPGetDatum(JsonbValueToJsonb(result.res)); +} + /* * Is the given type immutable when coming out of a JSONB context? * @@ -1201,7 +1214,6 @@ to_jsonb(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - JsonbInState result; JsonbTypeCategory tcategory; Oid outfuncoid; @@ -1213,11 +1225,7 @@ to_jsonb(PG_FUNCTION_ARGS) jsonb_categorize_type(val_type, &tcategory, &outfuncoid); - memset(&result, 0, sizeof(JsonbInState)); - - datum_to_jsonb(val, false, &result, tcategory, outfuncoid, false); - - PG_RETURN_POINTER(JsonbValueToJsonb(result.res)); + PG_RETURN_DATUM(to_jsonb_worker(val, tcategory, outfuncoid)); } Datum diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d3a973d86b..d1b03d6cb2 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -10832,6 +10832,15 @@ get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, case JSCTOR_JSON_ARRAY: funcname = "JSON_ARRAY"; break; + case JSCTOR_JSON_PARSE: + funcname = "JSON"; + break; + case JSCTOR_JSON_SCALAR: + funcname = "JSON_SCALAR"; + break; + case JSCTOR_JSON_SERIALIZE: + funcname = "JSON_SERIALIZE"; + break; default: elog(ERROR, "invalid JsonConstructorType %d", ctor->type); } @@ -10879,7 +10888,10 @@ get_json_constructor_options(JsonConstructorExpr *ctor, StringInfo buf) if (ctor->unique) appendStringInfoString(buf, " WITH UNIQUE KEYS"); - get_json_returning(ctor->returning, buf, true); + if (!((ctor->type == JSCTOR_JSON_PARSE || + ctor->type == JSCTOR_JSON_SCALAR) && + ctor->returning->typid == JSONOID)) + get_json_returning(ctor->returning, buf, true); } /* diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index efb5c3e098..d926713bd9 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1716,6 +1716,17 @@ typedef struct TriggerTransition /* Nodes for SQL/JSON support */ +/* + * JsonQuotes - + * representation of [KEEP|OMIT] QUOTES clause for JSON_QUERY() + */ +typedef enum JsonQuotes +{ + JS_QUOTES_UNSPEC, /* unspecified */ + JS_QUOTES_KEEP, /* KEEP QUOTES */ + JS_QUOTES_OMIT /* OMIT QUOTES */ +} JsonQuotes; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1739,6 +1750,43 @@ typedef struct JsonKeyValue JsonValueExpr *value; /* JSON value expression */ } JsonKeyValue; +/* + * JsonParseExpr - + * untransformed representation of JSON() + */ +typedef struct JsonParseExpr +{ + NodeTag type; + JsonValueExpr *expr; /* string expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + bool unique_keys; /* WITH UNIQUE KEYS? */ + int location; /* token location, or -1 if unknown */ +} JsonParseExpr; + +/* + * JsonScalarExpr - + * untransformed representation of JSON_SCALAR() + */ +typedef struct JsonScalarExpr +{ + NodeTag type; + Expr *expr; /* scalar expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonScalarExpr; + +/* + * JsonSerializeExpr - + * untransformed representation of JSON_SERIALIZE() function + */ +typedef struct JsonSerializeExpr +{ + NodeTag type; + JsonValueExpr *expr; /* json value expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonSerializeExpr; + /* * JsonObjectConstructor - * untransformed representation of JSON_OBJECT() constructor diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 792a743f72..6c6a1810af 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1607,7 +1607,10 @@ typedef enum JsonConstructorType JSCTOR_JSON_OBJECT = 1, JSCTOR_JSON_ARRAY = 2, JSCTOR_JSON_OBJECTAGG = 3, - JSCTOR_JSON_ARRAYAGG = 4 + JSCTOR_JSON_ARRAYAGG = 4, + JSCTOR_JSON_SCALAR = 5, + JSCTOR_JSON_SERIALIZE = 6, + JSCTOR_JSON_PARSE = 7 } JsonConstructorType; /* diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index f5b2e61ca5..5984dcfa4b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -230,11 +230,13 @@ PG_KEYWORD("is", IS, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("isnull", ISNULL, TYPE_FUNC_NAME_KEYWORD, AS_LABEL) PG_KEYWORD("isolation", ISOLATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) -PG_KEYWORD("json", JSON, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/json.h b/src/include/utils/json.h index 35a9a5545d..4c0e0bd09d 100644 --- a/src/include/utils/json.h +++ b/src/include/utils/json.h @@ -16,11 +16,30 @@ #include "lib/stringinfo.h" +typedef enum /* type categories for datum_to_json */ +{ + JSONTYPE_NULL, /* null, so we didn't bother to identify */ + JSONTYPE_BOOL, /* boolean (built-in types only) */ + JSONTYPE_NUMERIC, /* numeric (ditto) */ + JSONTYPE_DATE, /* we use special formatting for datetimes */ + JSONTYPE_TIMESTAMP, + JSONTYPE_TIMESTAMPTZ, + JSONTYPE_JSON, /* JSON itself (and JSONB) */ + JSONTYPE_ARRAY, /* array */ + JSONTYPE_COMPOSITE, /* composite */ + JSONTYPE_CAST, /* something with an explicit cast to JSON */ + JSONTYPE_OTHER /* all else */ +} JsonTypeCategory; + /* functions in json.c */ extern void escape_json(StringInfo buf, const char *str); extern char *JsonEncodeDateTime(char *buf, Datum value, Oid typid, const int *tzp); extern bool to_json_is_immutable(Oid typoid); +extern void json_categorize_type(Oid typoid, JsonTypeCategory *tcategory, + Oid *outfuncoid); +extern Datum to_json_worker(Datum val, JsonTypeCategory tcategory, + Oid outfuncoid); extern Datum json_build_object_worker(int nargs, Datum *args, bool *nulls, Oid *types, bool absent_on_null, bool unique_keys); diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index 649a1644f2..8417a74298 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -368,6 +368,22 @@ typedef struct JsonbIterator struct JsonbIterator *parent; } JsonbIterator; +/* unlike with json categories, we need to treat json and jsonb differently */ +typedef enum /* type categories for datum_to_jsonb */ +{ + JSONBTYPE_NULL, /* null, so we didn't bother to identify */ + JSONBTYPE_BOOL, /* boolean (built-in types only) */ + JSONBTYPE_NUMERIC, /* numeric (ditto) */ + JSONBTYPE_DATE, /* we use special formatting for datetimes */ + JSONBTYPE_TIMESTAMP, /* we use special formatting for timestamp */ + JSONBTYPE_TIMESTAMPTZ, /* ... and timestamptz */ + JSONBTYPE_JSON, /* JSON */ + JSONBTYPE_JSONB, /* JSONB */ + JSONBTYPE_ARRAY, /* array */ + JSONBTYPE_COMPOSITE, /* composite */ + JSONBTYPE_JSONCAST, /* something with an explicit cast to JSON */ + JSONBTYPE_OTHER /* all else */ +} JsonbTypeCategory; /* Convenience macros */ static inline Jsonb * @@ -418,6 +434,7 @@ extern void JsonbHashScalarValueExtended(const JsonbValue *scalarVal, uint64 *hash, uint64 seed); /* jsonb.c support functions */ +extern Datum jsonb_from_text(text *js, bool unique_keys); extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, @@ -430,6 +447,10 @@ extern Datum jsonb_set_element(Jsonb *jb, Datum *path, int path_len, extern Datum jsonb_get_element(Jsonb *jb, Datum *path, int npath, bool *isnull, bool as_text); extern bool to_jsonb_is_immutable(Oid typoid); +extern void jsonb_categorize_type(Oid typoid, JsonbTypeCategory *tcategory, + Oid *outfuncoid); +extern Datum to_jsonb_worker(Datum val, JsonbTypeCategory tcategory, + Oid outfuncoid); extern Datum jsonb_build_object_worker(int nargs, Datum *args, bool *nulls, Oid *types, bool absent_on_null, bool unique_keys); diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index d73c7e2c6c..ddea8a072f 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -1,3 +1,345 @@ +-- JSON() +SELECT JSON(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON(); + ^ +SELECT JSON(NULL); + json +------ + +(1 row) + +SELECT JSON('{ "a" : 1 } '); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +ERROR: JSON ENCODING clause is only allowed for bytea input type +LINE 1: SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); + ^ +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + pg_typeof +----------- + json +(1 row) + +SELECT JSON(' 1 '::json); + json +--------- + 1 +(1 row) + +SELECT JSON(' 1 '::jsonb); + json +------ + 1 +(1 row) + +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +ERROR: cannot use non-string types with WITH UNIQUE KEYS clause +LINE 1: SELECT JSON(' 1 '::json WITH UNIQUE KEYS); + ^ +SELECT JSON(123); +ERROR: cannot cast type integer to json +LINE 1: SELECT JSON(123); + ^ +SELECT JSON('{"a": 1, "a": 2}'); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +ERROR: duplicate JSON object key value +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); + QUERY PLAN +----------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); + QUERY PLAN +------------------------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON ENCODING UTF8) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); + QUERY PLAN +---------------------------------------------- + Result + Output: JSON('123'::text WITH UNIQUE KEYS) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +SELECT JSON('123' RETURNING text); +ERROR: cannot use RETURNING type text in JSON() +LINE 1: SELECT JSON('123' RETURNING text); + ^ +SELECT JSON('123' RETURNING text FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON() +LINE 1: SELECT JSON('123' RETURNING text FORMAT JSON); + ^ +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING json); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING jsonb); + QUERY PLAN +---------------------------------------------- + Result + Output: JSON('123'::jsonb RETURNING jsonb) +(2 rows) + +SELECT pg_typeof(JSON('123')); + pg_typeof +----------- + json +(1 row) + +SELECT pg_typeof(JSON('123' RETURNING json)); + pg_typeof +----------- + json +(1 row) + +SELECT pg_typeof(JSON('123' RETURNING jsonb)); + pg_typeof +----------- + jsonb +(1 row) + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SCALAR(); + ^ +SELECT JSON_SCALAR(NULL); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(NULL::int); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(123); + json_scalar +------------- + 123 +(1 row) + +SELECT JSON_SCALAR(123.45); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(123.45::numeric); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(true); + json_scalar +------------- + true +(1 row) + +SELECT JSON_SCALAR(false); + json_scalar +------------- + false +(1 row) + +SELECT JSON_SCALAR(' 123.45'); + json_scalar +------------- + " 123.45" +(1 row) + +SELECT JSON_SCALAR('2020-06-07'::date); + json_scalar +-------------- + "2020-06-07" +(1 row) + +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); + json_scalar +----------------------- + "2020-06-07T01:02:03" +(1 row) + +SELECT JSON_SCALAR('{}'::json); + json_scalar +------------- + {} +(1 row) + +SELECT JSON_SCALAR('{}'::jsonb); + json_scalar +------------- + {} +(1 row) + +SELECT JSON_SCALAR(123 RETURNING jsonb FORMAT JSON ENCODING UTF8); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_SCALAR() +LINE 1: SELECT JSON_SCALAR(123 RETURNING jsonb FORMAT JSON ENCODING ... + ^ +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); + QUERY PLAN +---------------------------- + Result + Output: JSON_SCALAR(123) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); + QUERY PLAN +------------------------------------ + Result + Output: JSON_SCALAR('123'::text) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING json); + QUERY PLAN +---------------------------- + Result + Output: JSON_SCALAR(123) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING jsonb); + QUERY PLAN +-------------------------------------------- + Result + Output: JSON_SCALAR(123 RETURNING jsonb) +(2 rows) + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SERIALIZE(); + ^ +SELECT JSON_SERIALIZE(NULL); + json_serialize +---------------- + +(1 row) + +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } '); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('1'); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('1' FORMAT JSON); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); + json_serialize +---------------------------- + \x7b20226122203a2031207d20 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + pg_typeof +----------- + text +(1 row) + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); +ERROR: cannot use RETURNING type jsonb in JSON_SERIALIZE() +HINT: Try returning a string type or bytea. +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); + QUERY PLAN +----------------------------------------------------- + Result + Output: JSON_SERIALIZE('{}'::json RETURNING text) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + QUERY PLAN +------------------------------------------------------ + Result + Output: JSON_SERIALIZE('{}'::json RETURNING bytea) +(2 rows) + -- JSON_OBJECT() SELECT JSON_OBJECT(); json_object @@ -630,6 +972,13 @@ ERROR: duplicate JSON object key SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); ERROR: duplicate JSON object key +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + json_objectagg +------------------ + {"1": 1, "2": 2} +(1 row) + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -645,6 +994,41 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); CREATE OR REPLACE VIEW public.json_object_view AS SELECT JSON_OBJECT('foo' : '1'::text FORMAT JSON, 'bar' : 'baz'::text RETURNING json) AS "json_object" DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + a | json_objectagg +---------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(2 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":1,"v":null} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1, "2" : 2 } + {"k":1,"v":null} | { "1" : 1, "2" : 2 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 4fd820fd51..f9afae0d42 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -1,3 +1,77 @@ +-- JSON() +SELECT JSON(); +SELECT JSON(NULL); +SELECT JSON('{ "a" : 1 } '); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + +SELECT JSON(' 1 '::json); +SELECT JSON(' 1 '::jsonb); +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +SELECT JSON(123); + +SELECT JSON('{"a": 1, "a": 2}'); +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + +SELECT JSON('123' RETURNING text); +SELECT JSON('123' RETURNING text FORMAT JSON); -- RETURNING FORMAT not allowed + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING json); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING jsonb); +SELECT pg_typeof(JSON('123')); +SELECT pg_typeof(JSON('123' RETURNING json)); +SELECT pg_typeof(JSON('123' RETURNING jsonb)); + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +SELECT JSON_SCALAR(NULL); +SELECT JSON_SCALAR(NULL::int); +SELECT JSON_SCALAR(123); +SELECT JSON_SCALAR(123.45); +SELECT JSON_SCALAR(123.45::numeric); +SELECT JSON_SCALAR(true); +SELECT JSON_SCALAR(false); +SELECT JSON_SCALAR(' 123.45'); +SELECT JSON_SCALAR('2020-06-07'::date); +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); +SELECT JSON_SCALAR('{}'::json); +SELECT JSON_SCALAR('{}'::jsonb); +SELECT JSON_SCALAR(123 RETURNING jsonb FORMAT JSON ENCODING UTF8); -- RETURNING FORMAT not allowed + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING json); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING jsonb); + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +SELECT JSON_SERIALIZE(NULL); +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); +SELECT JSON_SERIALIZE('{ "a" : 1 } '); +SELECT JSON_SERIALIZE('1'); +SELECT JSON_SERIALIZE('1' FORMAT JSON); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); + + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + -- JSON_OBJECT() SELECT JSON_OBJECT(); SELECT JSON_OBJECT(RETURNING json); @@ -216,6 +290,9 @@ FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -227,6 +304,24 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e941fb6c82..7d60511e9e 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1296,6 +1296,7 @@ JsonPathPredicateCallback JsonPathString JsonReturning JsonSemAction +JsonSerializeExpr JsonTokenType JsonTransformStringValuesAction JsonTypeCategory -- 2.35.3
v3-0005-JSON_TABLE.patch (application/octet-stream, 160.4K) download From c1b715dc0c241ca756550459e5e6703903b2db79 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:14:12 +0900 Subject: [PATCH v3 5/6] JSON_TABLE This feature allows jsonb data to be treated as a table and thus used in a FROM clause like other tabular data. Data can be selected from the jsonb using jsonpath expressions, and hoisted out of nested structures in the jsonb to form multiple rows, more or less like an outer join. This also adds the PLAN clauses for JSON_TABLE, which allow the user to specify how data from nested paths are joined, allowing considerable freedom in shaping the tabular output of JSON_TABLE. PLAN DEFAULT allows the user to specify the global strategies when dealing with sibling or child nested paths. The is often sufficient to achieve the necessary goal, and is considerably simpler than the full PLAN clause, which allows the user to specify the strategy to be used for each named nested path. Nikita Glukhov Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zhihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby. Discussion: https://postgr.es/m/7e2cb85d-24cf-4abb-30a5-1a33715959bd@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org --- doc/src/sgml/func.sgml | 448 ++++++++ src/backend/commands/explain.c | 8 +- src/backend/executor/execExprInterp.c | 5 + src/backend/executor/nodeTableFuncscan.c | 27 +- src/backend/nodes/makefuncs.c | 19 + src/backend/nodes/nodeFuncs.c | 30 + src/backend/parser/Makefile | 1 + src/backend/parser/gram.y | 476 ++++++++- src/backend/parser/meson.build | 1 + src/backend/parser/parse_clause.c | 14 +- src/backend/parser/parse_expr.c | 35 +- src/backend/parser/parse_jsontable.c | 739 +++++++++++++ src/backend/parser/parse_relation.c | 5 +- src/backend/parser/parse_target.c | 3 + src/backend/utils/adt/jsonpath_exec.c | 543 ++++++++++ src/backend/utils/adt/ruleutils.c | 279 ++++- src/include/nodes/execnodes.h | 2 + src/include/nodes/makefuncs.h | 2 + src/include/nodes/parsenodes.h | 90 ++ src/include/nodes/primnodes.h | 61 +- src/include/parser/kwlist.h | 4 + src/include/parser/parse_clause.h | 3 + src/include/utils/jsonpath.h | 3 + src/test/regress/expected/json_sqljson.out | 6 + src/test/regress/expected/jsonb_sqljson.out | 1069 +++++++++++++++++++ src/test/regress/sql/json_sqljson.sql | 4 + src/test/regress/sql/jsonb_sqljson.sql | 629 +++++++++++ src/tools/pgindent/typedefs.list | 13 + 28 files changed, 4486 insertions(+), 33 deletions(-) create mode 100644 src/backend/parser/parse_jsontable.c diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index cb36e1463b..44f016369e 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -17144,6 +17144,454 @@ array w/o UK? | t </sect2> + <sect2 id="functions-sqljson-table"> + <title>JSON_TABLE</title> + <indexterm> + <primary>json_table</primary> + </indexterm> + + <para> + <function>json_table</function> is an SQL/JSON function which + queries <acronym>JSON</acronym> data + and presents the results as a relational view, which can be accessed as a + regular SQL table. You can only use <function>json_table</function> inside the + <literal>FROM</literal> clause of a <literal>SELECT</literal> statement. + </para> + + <para> + Taking JSON data as input, <function>json_table</function> uses + a path expression to extract a part of the provided data that + will be used as a <firstterm>row pattern</firstterm> for the + constructed view. Each SQL/JSON item at the top level of the row pattern serves + as the source for a separate row in the constructed relational view. + </para> + + <para> + To split the row pattern into columns, <function>json_table</function> + provides the <literal>COLUMNS</literal> clause that defines the + schema of the created view. For each column to be constructed, + this clause provides a separate path expression that evaluates + the row pattern, extracts a JSON item, and returns it as a + separate SQL value for the specified column. If the required value + is stored in a nested level of the row pattern, it can be extracted + using the <literal>NESTED PATH</literal> subclause. Joining the + columns returned by <literal>NESTED PATH</literal> can add multiple + new rows to the constructed view. Such rows are called + <firstterm>child rows</firstterm>, as opposed to the <firstterm>parent row</firstterm> + that generates them. + </para> + + <para> + The rows produced by <function>JSON_TABLE</function> are laterally + joined to the row that generated them, so you do not have to explicitly join + the constructed view with the original table holding <acronym>JSON</acronym> + data. Optionally, you can specify how to join the columns returned + by <literal>NESTED PATH</literal> using the <literal>PLAN</literal> clause. + </para> + + <para> + Each <literal>NESTED PATH</literal> clause can generate one or more + columns. Columns produced by <literal>NESTED PATH</literal>s at the + same level are considered to be <firstterm>siblings</firstterm>, + while a column produced by a <literal>NESTED PATH</literal> is + considered to be a child of the column produced by a + <literal>NESTED PATH</literal> or row expression at a higher level. + Sibling columns are always joined first. Once they are processed, + the resulting rows are joined to the parent row. + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional></literal> + </term> + <listitem> + <para> + The input data to query, the JSON path expression defining the query, + and an optional <literal>PASSING</literal> clause, which can provide data + values to the <replaceable>path_expression</replaceable>. + The result of the input data + evaluation is called the <firstterm>row pattern</firstterm>. The row + pattern is used as the source for row values in the constructed view. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>COLUMNS</literal>( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + The <literal>COLUMNS</literal> clause defining the schema of the + constructed view. In this clause, you must specify all the columns + to be filled with SQL/JSON items. + The <replaceable>json_table_column</replaceable> + expression has the following syntax variants: + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>name</replaceable> <replaceable>type</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional></literal> + </term> + <listitem> + + <para> + Inserts a single SQL/JSON item into each row of + the specified column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable> + and fills the column with produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>ON EMPTY</literal> and + <literal>ON ERROR</literal> clauses to define how to handle missing values + or structural errors. + <literal>WRAPPER</literal> and <literal>QUOTES</literal> clauses can only + be used with JSON, array, and composite types. + These clauses have the same syntax and semantics as for + <function>json_value</function> and <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> <literal>FORMAT</literal> <replaceable>json_representation</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a composite SQL/JSON + item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable> + and fills the column with produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>WRAPPER</literal>, <literal>QUOTES</literal>, + <literal>ON EMPTY</literal> and <literal>ON ERROR</literal> clauses + to define additional settings for the returned SQL/JSON items. + These clauses have the same syntax and semantics as + for <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> + <literal>EXISTS</literal> <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a boolean item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable>, + checks whether any SQL/JSON items were returned, and fills the column with + resulting boolean value, one for each row. + The specified <replaceable>type</replaceable> should have cast from + <type>boolean</type>. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + </para> + <para> + Optionally, you can add <literal>ON ERROR</literal> clause to define + error behavior. This clause has the same syntax and semantics as + for <function>json_exists</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>NESTED PATH</literal> <replaceable>json_path_specification</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> + <literal>COLUMNS</literal> ( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + Extracts SQL/JSON items from nested levels of the row pattern, + generates one or more columns as defined by the <literal>COLUMNS</literal> + subclause, and inserts the extracted SQL/JSON items into each row of these columns. + The <replaceable>json_table_column</replaceable> expression in the + <literal>COLUMNS</literal> subclause uses the same syntax as in the + parent <literal>COLUMNS</literal> clause. + </para> + + <para> + The <literal>NESTED PATH</literal> syntax is recursive, + so you can go down multiple nested levels by specifying several + <literal>NESTED PATH</literal> subclauses within each other. + It allows to unnest the hierarchy of JSON objects and arrays + in a single function invocation rather than chaining several + <function>JSON_TABLE</function> expressions in an SQL statement. + </para> + + <para> + You can use the <literal>PLAN</literal> clause to define how + to join the columns returned by <literal>NESTED PATH</literal> clauses. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <literal>FOR ORDINALITY</literal> + </term> + <listitem> + + <para> + Adds an ordinality column that provides sequential row numbering. + You can have only one ordinality column per table. Row numbering + is 1-based. For child rows that result from the <literal>NESTED PATH</literal> + clauses, the parent row number is repeated. + </para> + </listitem> + </varlistentry> + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>AS</literal> <replaceable>json_path_name</replaceable> + </term> + <listitem> + + <para> + The optional <replaceable>json_path_name</replaceable> serves as an + identifier of the provided <replaceable>json_path_specification</replaceable>. + The path name must be unique and distinct from the column names. + When using the <literal>PLAN</literal> clause, you must specify the names + for all the paths, including the row pattern. Each path name can appear in + the <literal>PLAN</literal> clause only once. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN</literal> ( <replaceable>json_table_plan</replaceable> ) + </term> + <listitem> + + <para> + Defines how to join the data returned by <literal>NESTED PATH</literal> + clauses to the constructed view. + </para> + <para> + To join columns with parent/child relationship, you can use: + </para> + <variablelist> + <varlistentry> + <term> + <literal>INNER</literal> + </term> + <listitem> + + <para> + Use <literal>INNER JOIN</literal>, so that the parent row + is omitted from the output if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>OUTER</literal> + </term> + <listitem> + + <para> + Use <literal>LEFT OUTER JOIN</literal>, so that the parent row + is always included into the output even if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>, with NULL values + inserted into the child columns if the corresponding + values are missing. + </para> + <para> + This is the default option for joining columns with parent/child relationship. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para> + To join sibling columns, you can use: + </para> + + <variablelist> + <varlistentry> + <term> + <literal>UNION</literal> + </term> + <listitem> + + <para> + Generate one row for each value produced by each of the sibling + columns. The columns from the other siblings are set to null. + </para> + <para> + This is the default option for joining sibling columns. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>CROSS</literal> + </term> + <listitem> + + <para> + Generate one row for each combination of values from the sibling columns. + </para> + </listitem> + </varlistentry> + + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN DEFAULT</literal> ( <literal><replaceable>OUTER | INNER</replaceable> <optional>, <replaceable>UNION | CROSS</replaceable> </optional></literal> ) + </term> + <listitem> + <para> + The terms can also be specified in reverse order. The + <literal>INNER</literal> or <literal>OUTER</literal> option defines the + joining plan for parent/child columns, while <literal>UNION</literal> or + <literal>CROSS</literal> affects joins of sibling columns. This form + of <literal>PLAN</literal> overrides the default plan for + all columns at once. Even though the path names are not included in the + <literal>PLAN DEFAULT</literal> form, to conform to the SQL/JSON standard + they must be provided for all the paths if the <literal>PLAN</literal> + clause is used. + </para> + <para> + <literal>PLAN DEFAULT</literal> is simpler than specifying a complete + <literal>PLAN</literal>, and is often all that is required to get the desired + output. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para>Examples</para> + + <para> + In these examples the following small table storing some JSON data will be used: +<programlisting> +CREATE TABLE my_films ( js jsonb ); + +INSERT INTO my_films VALUES ( +'{ "favorites" : [ + { "kind" : "comedy", "films" : [ + { "title" : "Bananas", + "director" : "Woody Allen"}, + { "title" : "The Dinner Game", + "director" : "Francis Veber" } ] }, + { "kind" : "horror", "films" : [ + { "title" : "Psycho", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "thriller", "films" : [ + { "title" : "Vertigo", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "drama", "films" : [ + { "title" : "Yojimbo", + "director" : "Akira Kurosawa" } ] } + ] }'); +</programlisting> + </para> + <para> + Query the <structname>my_films</structname> table holding + some JSON data about the films and create a view that + distributes the film genre, title, and director between separate columns: +<screen> +SELECT jt.* FROM + my_films, + JSON_TABLE ( js, '$.favorites[*]' COLUMNS ( + id FOR ORDINALITY, + kind text PATH '$.kind', + NESTED PATH '$.films[*]' COLUMNS ( + title text PATH '$.title', + director text PATH '$.director'))) AS jt; +----+----------+------------------+------------------- + id | kind | title | director +----+----------+------------------+------------------- + 1 | comedy | Bananas | Woody Allen + 1 | comedy | The Dinner Game | Francis Veber + 2 | horror | Psycho | Alfred Hitchcock + 3 | thriller | Vertigo | Alfred Hitchcock + 4 | drama | Yojimbo | Akira Kurosawa + (5 rows) +</screen> + </para> + + <para> + Find a director that has done films in two different genres: +<screen> +SELECT + director1 AS director, title1, kind1, title2, kind2 +FROM + my_films, + JSON_TABLE ( js, '$.favorites' AS favs COLUMNS ( + NESTED PATH '$[*]' AS films1 COLUMNS ( + kind1 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film1 COLUMNS ( + title1 text PATH '$.title', + director1 text PATH '$.director') + ), + NESTED PATH '$[*]' AS films2 COLUMNS ( + kind2 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film2 COLUMNS ( + title2 text PATH '$.title', + director2 text PATH '$.director' + ) + ) + ) + PLAN (favs OUTER ((films1 INNER film1) CROSS (films2 INNER film2))) + ) AS jt + WHERE kind1 > kind2 AND director1 = director2; + + director | title1 | kind1 | title2 | kind2 +------------------+---------+----------+--------+-------- + Alfred Hitchcock | Vertigo | thriller | Psycho | horror +(1 row) +</screen> + </para> + </sect2> + <sect2 id="functions-sqljson-path"> <title>The SQL/JSON Path Language</title> diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 8570b14f62..ad899de5d6 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -3862,7 +3862,13 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es) break; case T_TableFuncScan: Assert(rte->rtekind == RTE_TABLEFUNC); - objectname = "xmltable"; + if (rte->tablefunc) + if (rte->tablefunc->functype == TFT_XMLTABLE) + objectname = "xmltable"; + else /* Must be TFT_JSON_TABLE */ + objectname = "json_table"; + else + objectname = NULL; objecttag = "Table Function Name"; break; case T_ValuesScan: diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 4b0647153a..584b993d0a 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -4308,6 +4308,11 @@ ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) break; } + case JSON_TABLE_OP: + res = item; + resnull = false; + break; + default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); *op->resnull = true; diff --git a/src/backend/executor/nodeTableFuncscan.c b/src/backend/executor/nodeTableFuncscan.c index 791cbd2372..085a612533 100644 --- a/src/backend/executor/nodeTableFuncscan.c +++ b/src/backend/executor/nodeTableFuncscan.c @@ -28,6 +28,7 @@ #include "miscadmin.h" #include "nodes/execnodes.h" #include "utils/builtins.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/xml.h" @@ -161,8 +162,9 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) scanstate->ss.ps.qual = ExecInitQual(node->scan.plan.qual, &scanstate->ss.ps); - /* Only XMLTABLE is supported currently */ - scanstate->routine = &XmlTableRoutine; + /* Only XMLTABLE and JSON_TABLE are supported currently */ + scanstate->routine = + tf->functype == TFT_XMLTABLE ? &XmlTableRoutine : &JsonbTableRoutine; scanstate->perTableCxt = AllocSetContextCreate(CurrentMemoryContext, @@ -182,6 +184,10 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) ExecInitExprList(tf->colexprs, (PlanState *) scanstate); scanstate->coldefexprs = ExecInitExprList(tf->coldefexprs, (PlanState *) scanstate); + scanstate->colvalexprs = + ExecInitExprList(tf->colvalexprs, (PlanState *) scanstate); + scanstate->passingvalexprs = + ExecInitExprList(tf->passingvalexprs, (PlanState *) scanstate); scanstate->notnulls = tf->notnulls; @@ -381,14 +387,17 @@ tfuncInitialize(TableFuncScanState *tstate, ExprContext *econtext, Datum doc) routine->SetNamespace(tstate, ns_name, ns_uri); } - /* Install the row filter expression into the table builder context */ - value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); - if (isnull) - ereport(ERROR, - (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), - errmsg("row filter expression must not be null"))); + if (routine->SetRowFilter) + { + /* Install the row filter expression into the table builder context */ + value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); + if (isnull) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("row filter expression must not be null"))); - routine->SetRowFilter(tstate, TextDatumGetCString(value)); + routine->SetRowFilter(tstate, TextDatumGetCString(value)); + } /* * Install the column filter expressions into the table builder context. diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 5dc3aa2e9f..84eb33b1db 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -874,6 +874,25 @@ makeJsonBehavior(JsonBehaviorType type, Node *default_expr) return behavior; } +/* + * makeJsonTableJoinedPlan - + * creates a joined JsonTablePlan node + */ +Node * +makeJsonTableJoinedPlan(JsonTablePlanJoinType type, Node *plan1, Node *plan2, + int location) +{ + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_JOINED; + n->join_type = type; + n->plan1 = castNode(JsonTablePlan, plan1); + n->plan2 = castNode(JsonTablePlan, plan2); + n->location = location; + + return (Node *) n; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index d31371bb4d..e07c066e15 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -2631,6 +2631,10 @@ expression_tree_walker_impl(Node *node, return true; if (WALK(tf->coldefexprs)) return true; + if (WALK(tf->colvalexprs)) + return true; + if (WALK(tf->passingvalexprs)) + return true; } break; default: @@ -3699,6 +3703,8 @@ expression_tree_mutator_impl(Node *node, MUTATE(newnode->rowexpr, tf->rowexpr, Node *); MUTATE(newnode->colexprs, tf->colexprs, List *); MUTATE(newnode->coldefexprs, tf->coldefexprs, List *); + MUTATE(newnode->colvalexprs, tf->colvalexprs, List *); + MUTATE(newnode->passingvalexprs, tf->passingvalexprs, List *); return (Node *) newnode; } break; @@ -4130,6 +4136,30 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonTable: + { + JsonTable *jt = (JsonTable *) node; + + if (WALK(jt->common)) + return true; + if (WALK(jt->columns)) + return true; + } + break; + case T_JsonTableColumn: + { + JsonTableColumn *jtc = (JsonTableColumn *) node; + + if (WALK(jtc->typeName)) + return true; + if (WALK(jtc->on_empty)) + return true; + if (WALK(jtc->on_error)) + return true; + if (jtc->coltype == JTC_NESTED && WALK(jtc->columns)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile index 9f1c4022bb..f4c0cc7f10 100644 --- a/src/backend/parser/Makefile +++ b/src/backend/parser/Makefile @@ -23,6 +23,7 @@ OBJS = \ parse_enr.o \ parse_expr.o \ parse_func.o \ + parse_jsontable.o \ parse_merge.o \ parse_node.o \ parse_oper.o \ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index f7ad8f18de..ff1d3332c1 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -654,19 +654,43 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_aggregate_func json_api_common_syntax json_argument + json_table + json_table_column_definition + json_table_ordinality_column_definition + json_table_regular_column_definition + json_table_formatted_column_definition + json_table_exists_column_definition + json_table_nested_columns + json_table_plan_clause_opt + json_table_plan + json_table_plan_simple + json_table_plan_parent_child + json_table_plan_outer + json_table_plan_inner + json_table_plan_sibling + json_table_plan_union + json_table_plan_cross + json_table_plan_primary %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt json_arguments + json_table_columns_clause + json_table_column_definition_list +%type <str> json_table_column_path_specification_clause_opt %type <ival> json_encoding_clause_opt json_predicate_type_constraint json_wrapper_behavior + json_table_default_plan_choices + json_table_default_plan_inner_outer + json_table_default_plan_union_cross %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt %type <jsbehavior> json_value_behavior json_query_behavior json_exists_behavior + json_table_behavior %type <js_quotes> json_quotes_clause_opt /* @@ -732,7 +756,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG - JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_TABLE JSON_VALUE KEY KEYS KEEP @@ -743,8 +767,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE - NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE - NORMALIZE NORMALIZED + NAME_P NAMES NATIONAL NATURAL NCHAR NESTED NEW NEXT NFC NFD NFKC NFKD NO + NONE NORMALIZE NORMALIZED NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC @@ -752,8 +776,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER - PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD - PLACING PLANS POLICY + PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH + PLACING PLAN PLANS POLICY POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION @@ -861,6 +885,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); * Using the same precedence as IDENT seems right for the reasons given above. */ %nonassoc UNBOUNDED /* ideally would have same precedence as IDENT */ +%nonassoc COLUMNS %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' @@ -883,6 +908,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL +%nonassoc json_table_column +%nonassoc NESTED +%left PATH %% /* @@ -13324,6 +13352,21 @@ table_ref: relation_expr opt_alias_clause $2->alias = $4; $$ = (Node *) $2; } + | json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $1); + + jt->alias = $2; + $$ = (Node *) jt; + } + | LATERAL_P json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $2); + + jt->alias = $3; + jt->lateral = true; + $$ = (Node *) jt; + } ; @@ -13891,6 +13934,8 @@ xmltable_column_option_el: { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(true), @1); } | NULL_P { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(false), @1); } + | PATH b_expr + { $$ = makeDefElem("path", $2, @1); } ; xml_namespace_list: @@ -16697,6 +16742,11 @@ json_value_behavior: | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } ; +json_table_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + ; + /* ARRAY is a noise word */ json_wrapper_behavior: WITHOUT WRAPPER { $$ = JSW_NONE; } @@ -16718,6 +16768,414 @@ json_quotes_clause_opt: | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } ; +json_table: + JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + json_table_behavior ON ERROR_P + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_columns_clause: + COLUMNS '(' json_table_column_definition_list ')' { $$ = $3; } + ; + +json_table_column_definition_list: + json_table_column_definition + { $$ = list_make1($1); } + | json_table_column_definition_list ',' json_table_column_definition + { $$ = lappend($1, $3); } + ; + +json_table_column_definition: + json_table_ordinality_column_definition %prec json_table_column + | json_table_regular_column_definition %prec json_table_column + | json_table_formatted_column_definition %prec json_table_column + | json_table_exists_column_definition %prec json_table_column + | json_table_nested_columns + ; + +json_table_ordinality_column_definition: + ColId FOR ORDINALITY + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FOR_ORDINALITY; + n->name = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_regular_column_definition: + ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_exists_column_definition: + ColId Typename + EXISTS json_table_column_path_specification_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + EXISTS json_table_column_path_specification_clause_opt + json_exists_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_column_path_specification_clause_opt: + PATH Sconst { $$ = $2; } + | /* EMPTY */ %prec json_table_column { $$ = NULL; } + ; + +json_table_formatted_column_definition: + ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->on_error = $12; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_nested_columns: + NESTED path_opt Sconst + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = NULL; + n->columns = $4; + n->location = @1; + $$ = (Node *) n; + } + | NESTED path_opt Sconst AS name + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = $5; + n->columns = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +path_opt: + PATH { } + | /* EMPTY */ { } + ; + +json_table_plan_clause_opt: + PLAN '(' json_table_plan ')' { $$ = $3; } + | PLAN DEFAULT '(' json_table_default_plan_choices ')' + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_DEFAULT; + n->join_type = $4; + n->location = @1; + $$ = (Node *) n; + } + | /* EMPTY */ { $$ = NULL; } + ; + +json_table_plan: + json_table_plan_simple + | json_table_plan_parent_child + | json_table_plan_sibling + ; + +json_table_plan_simple: + name + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_SIMPLE; + n->pathname = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_plan_primary: + json_table_plan_simple { $$ = $1; } + | '(' json_table_plan ')' + { + castNode(JsonTablePlan, $2)->location = @1; + $$ = $2; + } + ; + +json_table_plan_outer: + json_table_plan_simple OUTER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_OUTER, $1, $3, @1); } + ; + +json_table_plan_inner: + json_table_plan_simple INNER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_INNER, $1, $3, @1); } + ; + +json_table_plan_parent_child: + json_table_plan_outer + | json_table_plan_inner + ; + +json_table_plan_union: + json_table_plan_primary UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + | json_table_plan_union UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + ; + +json_table_plan_cross: + json_table_plan_primary CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + | json_table_plan_cross CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + ; + +json_table_plan_sibling: + json_table_plan_union + | json_table_plan_cross + ; + +json_table_default_plan_choices: + json_table_default_plan_inner_outer { $$ = $1 | JSTPJ_UNION; } + | json_table_default_plan_union_cross { $$ = $1 | JSTPJ_OUTER; } + | json_table_default_plan_inner_outer ',' + json_table_default_plan_union_cross { $$ = $1 | $3; } + | json_table_default_plan_union_cross ',' + json_table_default_plan_inner_outer { $$ = $1 | $3; } + ; + +json_table_default_plan_inner_outer: + INNER_P { $$ = JSTPJ_INNER; } + | OUTER_P { $$ = JSTPJ_OUTER; } + ; + +json_table_default_plan_union_cross: + UNION { $$ = JSTPJ_UNION; } + | CROSS { $$ = JSTPJ_CROSS; } + ; + json_returning_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17442,6 +17900,7 @@ unreserved_keyword: | MOVE | NAME_P | NAMES + | NESTED | NEW | NEXT | NFC @@ -17476,6 +17935,8 @@ unreserved_keyword: | PARTITION | PASSING | PASSWORD + | PATH + | PLAN | PLANS | POLICY | PRECEDING @@ -17640,6 +18101,7 @@ col_name_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | LEAST | NATIONAL @@ -18008,6 +18470,7 @@ bare_label_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | KEEP | KEY @@ -18047,6 +18510,7 @@ bare_label_keyword: | NATIONAL | NATURAL | NCHAR + | NESTED | NEW | NEXT | NFC @@ -18091,7 +18555,9 @@ bare_label_keyword: | PARTITION | PASSING | PASSWORD + | PATH | PLACING + | PLAN | PLANS | POLICY | POSITION diff --git a/src/backend/parser/meson.build b/src/backend/parser/meson.build index 94517a7f7d..0f0e4ac7ca 100644 --- a/src/backend/parser/meson.build +++ b/src/backend/parser/meson.build @@ -10,6 +10,7 @@ backend_sources += files( 'parse_enr.c', 'parse_expr.c', 'parse_func.c', + 'parse_jsontable.c', 'parse_merge.c', 'parse_node.c', 'parse_oper.c', diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index f61f794755..b44ff44991 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -697,7 +697,11 @@ transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf) char **names; int colno; - /* Currently only XMLTABLE is supported */ + /* + * Currently we only support XMLTABLE here. See transformJsonTable() for + * JSON_TABLE support. + */ + tf->functype = TFT_XMLTABLE; constructName = "XMLTABLE"; docType = XMLOID; @@ -1104,13 +1108,17 @@ transformFromClauseItem(ParseState *pstate, Node *n, rtr->rtindex = nsitem->p_rtindex; return (Node *) rtr; } - else if (IsA(n, RangeTableFunc)) + else if (IsA(n, RangeTableFunc) || IsA(n, JsonTable)) { /* table function is like a plain relation */ RangeTblRef *rtr; ParseNamespaceItem *nsitem; - nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + if (IsA(n, RangeTableFunc)) + nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + else + nsitem = transformJsonTable(pstate, (JsonTable *) n); + *top_nsitem = nsitem; *namespace = list_make1(nsitem); rtr = makeNode(RangeTblRef); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 2791ce85f4..72ee12bba7 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4275,7 +4275,7 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) JsonFormatType format; char *constructName; - if (func->common->pathname) + if (func->common->pathname && func->op != JSON_TABLE_OP) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("JSON_TABLE path name is not allowed here"), @@ -4294,6 +4294,9 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) case JSON_EXISTS_OP: constructName = "JSON_EXISTS()"; break; + case JSON_TABLE_OP: + constructName = "JSON_TABLE()"; + break; default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); break; @@ -4332,14 +4335,19 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) transformJsonPassingArgs(pstate, format, func->common->passing, &jsexpr->passing_values, &jsexpr->passing_names); - if (func->op != JSON_EXISTS_OP) + if (func->op != JSON_EXISTS_OP && func->op != JSON_TABLE_OP) jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, JSON_BEHAVIOR_NULL); - jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, - func->op == JSON_EXISTS_OP ? - JSON_BEHAVIOR_FALSE : - JSON_BEHAVIOR_NULL); + if (func->op == JSON_EXISTS_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_FALSE); + else if (func->op == JSON_TABLE_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_EMPTY); + else + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_NULL); return jsexpr; } @@ -4659,6 +4667,21 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->result_coercion->expr = NULL; } break; + + case JSON_TABLE_OP: + jsexpr->returning = makeNode(JsonReturning); + jsexpr->returning->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + jsexpr->returning->typid = exprType(contextItemExpr); + jsexpr->returning->typmod = -1; + + if (jsexpr->returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("JSON_TABLE() is not yet implemented for the json type"), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + break; } if (exprType(contextItemExpr) != JSONBOID) diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c new file mode 100644 index 0000000000..a7802e0499 --- /dev/null +++ b/src/backend/parser/parse_jsontable.c @@ -0,0 +1,739 @@ +/*------------------------------------------------------------------------- + * + * parse_jsontable.c + * parsing of JSON_TABLE + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/parser/parse_jsontable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/pg_collation.h" +#include "catalog/pg_type.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "optimizer/optimizer.h" +#include "parser/parse_clause.h" +#include "parser/parse_collate.h" +#include "parser/parse_expr.h" +#include "parser/parse_relation.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/json.h" +#include "utils/lsyscache.h" + +/* Context for JSON_TABLE transformation */ +typedef struct JsonTableParseContext +{ + ParseState *pstate; /* parsing state */ + JsonTable *table; /* untransformed node */ + TableFunc *tablefunc; /* transformed node */ + List *pathNames; /* list of all path and columns names */ + int pathNameId; /* path name id counter */ + Oid contextItemTypid; /* type oid of context item (json/jsonb) */ +} JsonTableParseContext; + +static JsonTableParent *transformJsonTableColumns(JsonTableParseContext *cxt, + JsonTablePlan *plan, + List *columns, + char *pathSpec, + char **pathName, + int location); + +static Node * +makeStringConst(char *str, int location) +{ + A_Const *n = makeNode(A_Const); + + n->val.node.type = T_String; + n->val.sval.sval = str; + n->location = location; + + return (Node *) n; +} + +/* + * Transform JSON_TABLE column + * - regular column into JSON_VALUE() + * - FORMAT JSON column into JSON_QUERY() + * - EXISTS column into JSON_EXISTS() + */ +static Node * +transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, + List *passingArgs, bool errorOnError) +{ + JsonFuncExpr *jfexpr = makeNode(JsonFuncExpr); + JsonCommon *common = makeNode(JsonCommon); + JsonOutput *output = makeNode(JsonOutput); + char *pathspec; + JsonFormat *default_format; + + jfexpr->op = + jtc->coltype == JTC_REGULAR ? JSON_VALUE_OP : + jtc->coltype == JTC_EXISTS ? JSON_EXISTS_OP : JSON_QUERY_OP; + jfexpr->common = common; + jfexpr->output = output; + jfexpr->on_empty = jtc->on_empty; + jfexpr->on_error = jtc->on_error; + if (!jfexpr->on_error && errorOnError) + jfexpr->on_error = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); + jfexpr->omit_quotes = jtc->omit_quotes; + jfexpr->wrapper = jtc->wrapper; + jfexpr->location = jtc->location; + + output->typeName = jtc->typeName; + output->returning = makeNode(JsonReturning); + output->returning->format = jtc->format; + + default_format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + + common->pathname = NULL; + common->expr = makeJsonValueExpr((Expr *) contextItemExpr, default_format); + common->passing = passingArgs; + + if (jtc->pathspec) + pathspec = jtc->pathspec; + else + { + /* Construct default path as '$."column_name"' */ + StringInfoData path; + + initStringInfo(&path); + + appendStringInfoString(&path, "$."); + escape_json(&path, jtc->name); + + pathspec = path.data; + } + + common->pathspec = makeStringConst(pathspec, -1); + + return (Node *) jfexpr; +} + +static bool +isJsonTablePathNameDuplicate(JsonTableParseContext *cxt, const char *pathname) +{ + ListCell *lc; + + foreach(lc, cxt->pathNames) + { + if (!strcmp(pathname, (const char *) lfirst(lc))) + return true; + } + + return false; +} + +/* Register the column name in the path name list. */ +static void +registerJsonTableColumn(JsonTableParseContext *cxt, char *colname) +{ + if (isJsonTablePathNameDuplicate(cxt, colname)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_ALIAS), + errmsg("duplicate JSON_TABLE column name: %s", colname), + errhint("JSON_TABLE column names must be distinct from one another."))); + + cxt->pathNames = lappend(cxt->pathNames, colname); +} + +/* Recursively register all nested column names in the path name list. */ +static void +registerAllJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED) + { + if (jtc->pathname) + registerJsonTableColumn(cxt, jtc->pathname); + + registerAllJsonTableColumns(cxt, jtc->columns); + } + else + { + registerJsonTableColumn(cxt, jtc->name); + } + } +} + +/* Generate a new unique JSON_TABLE path name. */ +static char * +generateJsonTablePathName(JsonTableParseContext *cxt) +{ + char namebuf[32]; + char *name = namebuf; + + do + { + snprintf(namebuf, sizeof(namebuf), "json_table_path_%d", + ++cxt->pathNameId); + } while (isJsonTablePathNameDuplicate(cxt, name)); + + name = pstrdup(name); + cxt->pathNames = lappend(cxt->pathNames, name); + + return name; +} + +/* Collect sibling path names from plan to the specified list. */ +static void +collectSiblingPathsInJsonTablePlan(JsonTablePlan *plan, List **paths) +{ + if (plan->plan_type == JSTP_SIMPLE) + *paths = lappend(*paths, plan->pathname); + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + *paths = lappend(*paths, plan->plan1->pathname); + } + else if (plan->join_type == JSTPJ_CROSS || + plan->join_type == JSTPJ_UNION) + { + collectSiblingPathsInJsonTablePlan(plan->plan1, paths); + collectSiblingPathsInJsonTablePlan(plan->plan2, paths); + } + else + elog(ERROR, "invalid JSON_TABLE join type %d", + plan->join_type); + } +} + +/* + * Validate child JSON_TABLE plan by checking that: + * - all nested columns have path names specified + * - all nested columns have corresponding node in the sibling plan + * - plan does not contain duplicate or extra nodes + */ +static void +validateJsonTableChildPlan(ParseState *pstate, JsonTablePlan *plan, + List *columns) +{ + ListCell *lc1; + List *siblings = NIL; + int nchildren = 0; + + if (plan) + collectSiblingPathsInJsonTablePlan(plan, &siblings); + + foreach(lc1, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc1)); + + if (jtc->coltype == JTC_NESTED) + { + ListCell *lc2; + bool found = false; + + if (!jtc->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("nested JSON_TABLE columns must contain an explicit AS pathname specification if an explicit PLAN clause is used"), + parser_errposition(pstate, jtc->location))); + + /* find nested path name in the list of sibling path names */ + foreach(lc2, siblings) + { + if ((found = !strcmp(jtc->pathname, lfirst(lc2)))) + break; + } + + if (!found) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node for nested path %s was not found in plan.", jtc->pathname), + parser_errposition(pstate, jtc->location))); + + nchildren++; + } + } + + if (list_length(siblings) > nchildren) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node contains some extra or duplicate sibling nodes."), + parser_errposition(pstate, plan ? plan->location : -1))); +} + +static JsonTableColumn * +findNestedJsonTableColumn(List *columns, const char *pathname) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED && + jtc->pathname && + !strcmp(jtc->pathname, pathname)) + return jtc; + } + + return NULL; +} + +static Node * +transformNestedJsonTableColumn(JsonTableParseContext *cxt, JsonTableColumn *jtc, + JsonTablePlan *plan) +{ + JsonTableParent *node; + char *pathname = jtc->pathname; + + node = transformJsonTableColumns(cxt, plan, jtc->columns, jtc->pathspec, + &pathname, jtc->location); + + return (Node *) node; +} + +static Node * +makeJsonTableSiblingJoin(bool cross, Node *lnode, Node *rnode) +{ + JsonTableSibling *join = makeNode(JsonTableSibling); + + join->larg = lnode; + join->rarg = rnode; + join->cross = cross; + + return (Node *) join; +} + +/* + * Recursively transform child JSON_TABLE plan. + * + * Default plan is transformed into a cross/union join of its nested columns. + * Simple and outer/inner plans are transformed into a JsonTableParent by + * finding and transforming corresponding nested column. + * Sibling plans are recursively transformed into a JsonTableSibling. + */ +static Node * +transformJsonTableChildPlan(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns) +{ + JsonTableColumn *jtc = NULL; + + if (!plan || plan->plan_type == JSTP_DEFAULT) + { + /* unspecified or default plan */ + Node *res = NULL; + ListCell *lc; + bool cross = plan && (plan->join_type & JSTPJ_CROSS); + + /* transform all nested columns into cross/union join */ + foreach(lc, columns) + { + JsonTableColumn *col = castNode(JsonTableColumn, lfirst(lc)); + Node *node; + + if (col->coltype != JTC_NESTED) + continue; + + node = transformNestedJsonTableColumn(cxt, col, plan); + + /* join transformed node with previous sibling nodes */ + res = res ? makeJsonTableSiblingJoin(cross, res, node) : node; + } + + return res; + } + else if (plan->plan_type == JSTP_SIMPLE) + { + jtc = findNestedJsonTableColumn(columns, plan->pathname); + } + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + jtc = findNestedJsonTableColumn(columns, plan->plan1->pathname); + } + else + { + Node *node1 = transformJsonTableChildPlan(cxt, plan->plan1, + columns); + Node *node2 = transformJsonTableChildPlan(cxt, plan->plan2, + columns); + + return makeJsonTableSiblingJoin(plan->join_type == JSTPJ_CROSS, + node1, node2); + } + } + else + elog(ERROR, "invalid JSON_TABLE plan type %d", plan->plan_type); + + if (!jtc) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name was %s not found in nested columns list.", + plan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + return transformNestedJsonTableColumn(cxt, jtc, plan); +} + +/* Check whether type is json/jsonb, array, or record. */ +static bool +typeIsComposite(Oid typid) +{ + char typtype; + + if (typid == JSONOID || + typid == JSONBOID || + typid == RECORDOID || + type_is_array(typid)) + return true; + + typtype = get_typtype(typid); + + if (typtype == TYPTYPE_COMPOSITE) + return true; + + if (typtype == TYPTYPE_DOMAIN) + return typeIsComposite(getBaseType(typid)); + + return false; +} + +/* Append transformed non-nested JSON_TABLE columns to the TableFunc node */ +static void +appendJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *col; + ParseState *pstate = cxt->pstate; + JsonTable *jt = cxt->table; + TableFunc *tf = cxt->tablefunc; + bool errorOnError = jt->on_error && + jt->on_error->btype == JSON_BEHAVIOR_ERROR; + + foreach(col, columns) + { + JsonTableColumn *rawc = castNode(JsonTableColumn, lfirst(col)); + Oid typid; + int32 typmod; + Node *colexpr; + + if (rawc->name) + { + /* make sure column names are unique */ + ListCell *colname; + + foreach(colname, tf->colnames) + if (!strcmp((const char *) colname, rawc->name)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("column name \"%s\" is not unique", + rawc->name), + parser_errposition(pstate, rawc->location))); + + tf->colnames = lappend(tf->colnames, + makeString(pstrdup(rawc->name))); + } + + /* + * Determine the type and typmod for the new column. FOR ORDINALITY + * columns are INTEGER by standard; the others are user-specified. + */ + switch (rawc->coltype) + { + case JTC_FOR_ORDINALITY: + colexpr = NULL; + typid = INT4OID; + typmod = -1; + break; + + case JTC_REGULAR: + typenameTypeIdAndMod(pstate, rawc->typeName, &typid, &typmod); + + /* + * Use implicit FORMAT JSON for composite types (arrays and + * records) + */ + if (typeIsComposite(typid)) + rawc->coltype = JTC_FORMATTED; + else if (rawc->wrapper != JSW_NONE) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use WITH WRAPPER clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + else if (rawc->omit_quotes) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use OMIT QUOTES clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + + /* FALLTHROUGH */ + case JTC_EXISTS: + case JTC_FORMATTED: + { + Node *je; + CaseTestExpr *param = makeNode(CaseTestExpr); + + param->collation = InvalidOid; + param->typeId = cxt->contextItemTypid; + param->typeMod = -1; + + je = transformJsonTableColumn(rawc, (Node *) param, + NIL, errorOnError); + + colexpr = transformExpr(pstate, je, EXPR_KIND_FROM_FUNCTION); + assign_expr_collations(pstate, colexpr); + + typid = exprType(colexpr); + typmod = exprTypmod(colexpr); + break; + } + + case JTC_NESTED: + continue; + + default: + elog(ERROR, "unknown JSON_TABLE column type: %d", rawc->coltype); + break; + } + + tf->coltypes = lappend_oid(tf->coltypes, typid); + tf->coltypmods = lappend_int(tf->coltypmods, typmod); + tf->colcollations = lappend_oid(tf->colcollations, get_typcollation(typid)); + tf->colvalexprs = lappend(tf->colvalexprs, colexpr); + } +} + +/* + * Create transformed JSON_TABLE parent plan node by appending all non-nested + * columns to the TableFunc node and remembering their indices in the + * colvalexprs list. + */ +static JsonTableParent * +makeParentJsonTableNode(JsonTableParseContext *cxt, char *pathSpec, char *pathName, + List *columns) +{ + JsonTableParent *node = makeNode(JsonTableParent); + + node->path = makeNode(JsonTablePath); + node->path->value = makeConst(JSONPATHOID, -1, InvalidOid, -1, + DirectFunctionCall1(jsonpath_in, + CStringGetDatum(pathSpec)), + false, false); + if (pathName) + node->path->name = pstrdup(pathName); + + /* save start of column range */ + node->colMin = list_length(cxt->tablefunc->colvalexprs); + + appendJsonTableColumns(cxt, columns); + + /* save end of column range */ + node->colMax = list_length(cxt->tablefunc->colvalexprs) - 1; + + node->errorOnError = + cxt->table->on_error && + cxt->table->on_error->btype == JSON_BEHAVIOR_ERROR; + + return node; +} + +static JsonTableParent * +transformJsonTableColumns(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns, char *pathSpec, char **pathName, + int location) +{ + JsonTableParent *node; + JsonTablePlan *childPlan; + bool defaultPlan = !plan || plan->plan_type == JSTP_DEFAULT; + + if (!*pathName) + { + if (cxt->table->plan) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE expression"), + errdetail("JSON_TABLE columns must contain " + "explicit AS pathname specification if " + "explicit PLAN clause is used"), + parser_errposition(cxt->pstate, location))); + + *pathName = generateJsonTablePathName(cxt); + } + + if (defaultPlan) + childPlan = plan; + else + { + /* validate parent and child plans */ + JsonTablePlan *parentPlan; + + if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type != JSTPJ_INNER && + plan->join_type != JSTPJ_OUTER) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Expected INNER or OUTER JSON_TABLE plan node."), + parser_errposition(cxt->pstate, plan->location))); + + parentPlan = plan->plan1; + childPlan = plan->plan2; + + Assert(parentPlan->plan_type != JSTP_JOINED); + Assert(parentPlan->pathname); + } + else + { + parentPlan = plan; + childPlan = NULL; + } + + if (strcmp(parentPlan->pathname, *pathName)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name mismatch: expected %s but %s is given.", + *pathName, parentPlan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + validateJsonTableChildPlan(cxt->pstate, childPlan, columns); + } + + /* transform only non-nested columns */ + node = makeParentJsonTableNode(cxt, pathSpec, *pathName, columns); + + if (childPlan || defaultPlan) + { + /* transform recursively nested columns */ + node->child = transformJsonTableChildPlan(cxt, childPlan, columns); + if (node->child) + node->outerJoin = !plan || (plan->join_type & JSTPJ_OUTER); + /* else: default plan case, no children found */ + } + + return node; +} + +/* + * transformJsonTable - + * Transform a raw JsonTable into TableFunc. + * + * Transform the document-generating expression, the row-generating expression, + * the column-generating expressions, and the default value expressions. + */ +ParseNamespaceItem * +transformJsonTable(ParseState *pstate, JsonTable *jt) +{ + JsonTableParseContext cxt; + TableFunc *tf = makeNode(TableFunc); + JsonFuncExpr *jfe = makeNode(JsonFuncExpr); + JsonExpr *je; + JsonTablePlan *plan = jt->plan; + JsonCommon *jscommon; + char *rootPathName = jt->common->pathname; + char *rootPath; + bool is_lateral; + + cxt.pstate = pstate; + cxt.table = jt; + cxt.tablefunc = tf; + cxt.pathNames = NIL; + cxt.pathNameId = 0; + + if (rootPathName) + registerJsonTableColumn(&cxt, rootPathName); + + registerAllJsonTableColumns(&cxt, jt->columns); + +#if 0 /* XXX it' unclear from the standard whether + * root path name is mandatory or not */ + if (plan && plan->plan_type != JSTP_DEFAULT && !rootPathName) + { + /* Assign root path name and create corresponding plan node */ + JsonTablePlan *rootNode = makeNode(JsonTablePlan); + JsonTablePlan *rootPlan = (JsonTablePlan *) + makeJsonTableJoinedPlan(JSTPJ_OUTER, (Node *) rootNode, + (Node *) plan, jt->location); + + rootPathName = generateJsonTablePathName(&cxt); + + rootNode->plan_type = JSTP_SIMPLE; + rootNode->pathname = rootPathName; + + plan = rootPlan; + } +#endif + + jscommon = copyObject(jt->common); + jscommon->pathspec = makeStringConst(pstrdup("$"), -1); + + jfe->op = JSON_TABLE_OP; + jfe->common = jscommon; + jfe->on_error = jt->on_error; + jfe->location = jt->common->location; + + /* + * We make lateral_only names of this level visible, whether or not the + * RangeTableFunc is explicitly marked LATERAL. This is needed for SQL + * spec compliance and seems useful on convenience grounds for all + * functions in FROM. + * + * (LATERAL can't nest within a single pstate level, so we don't need + * save/restore logic here.) + */ + Assert(!pstate->p_lateral_active); + pstate->p_lateral_active = true; + + tf->functype = TFT_JSON_TABLE; + tf->docexpr = transformExpr(pstate, (Node *) jfe, EXPR_KIND_FROM_FUNCTION); + + cxt.contextItemTypid = exprType(tf->docexpr); + + if (!IsA(jt->common->pathspec, A_Const) || + castNode(A_Const, jt->common->pathspec)->val.node.type != T_String) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("only string constants supported in JSON_TABLE path specification"), + parser_errposition(pstate, + exprLocation(jt->common->pathspec)))); + + rootPath = castNode(A_Const, jt->common->pathspec)->val.sval.sval; + + tf->plan = (Node *) transformJsonTableColumns(&cxt, plan, jt->columns, + rootPath, &rootPathName, + jt->common->location); + + /* Also save a copy of the PASSING arguments in the TableFunc node. */ + je = (JsonExpr *) tf->docexpr; + tf->passingvalexprs = copyObject(je->passing_values); + + tf->ordinalitycol = -1; /* undefine ordinality column number */ + tf->location = jt->location; + + pstate->p_lateral_active = false; + + /* + * Mark the RTE as LATERAL if the user said LATERAL explicitly, or if + * there are any lateral cross-references in it. + */ + is_lateral = jt->lateral || contain_vars_of_level((Node *) tf, 0); + + return addRangeTableEntryForTableFunc(pstate, + tf, jt->alias, is_lateral, true); +} diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 41d60494b9..7da42c4772 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -2072,7 +2072,8 @@ addRangeTableEntryForTableFunc(ParseState *pstate, Assert(list_length(tf->coltypmods) == list_length(tf->colnames)); Assert(list_length(tf->colcollations) == list_length(tf->colnames)); - refname = alias ? alias->aliasname : pstrdup("xmltable"); + refname = alias ? alias->aliasname : + pstrdup(tf->functype == TFT_XMLTABLE ? "xmltable" : "json_table"); rte->rtekind = RTE_TABLEFUNC; rte->relid = InvalidOid; @@ -2095,7 +2096,7 @@ addRangeTableEntryForTableFunc(ParseState *pstate, ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), errmsg("%s function has %d columns available but %d columns specified", - "XMLTABLE", + tf->functype == TFT_XMLTABLE ? "XMLTABLE" : "JSON_TABLE", list_length(tf->colnames), numaliases))); rte->eref = eref; diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4f94fc69d6..6500e42485 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1992,6 +1992,9 @@ FigureColnameInternal(Node *node, char **name) case JSON_EXISTS_OP: *name = "json_exists"; return 2; + case JSON_TABLE_OP: + *name = "json_table"; + return 2; } break; default: diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index eded22e194..92d76d5cde 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -61,10 +61,12 @@ #include "catalog/pg_collation.h" #include "catalog/pg_type.h" +#include "executor/execExpr.h" #include "funcapi.h" #include "lib/stringinfo.h" #include "miscadmin.h" #include "nodes/miscnodes.h" +#include "nodes/nodeFuncs.h" #include "regex/regex.h" #include "utils/builtins.h" #include "utils/date.h" @@ -75,6 +77,8 @@ #include "utils/guc.h" #include "utils/json.h" #include "utils/jsonpath.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" #include "utils/timestamp.h" #include "utils/varlena.h" @@ -156,6 +160,61 @@ typedef struct JsonValueListIterator ListCell *next; } JsonValueListIterator; +/* Structures for JSON_TABLE execution */ + +typedef enum JsonTablePlanStateType +{ + JSON_TABLE_SCAN_STATE = 0, + JSON_TABLE_JOIN_STATE +} JsonTablePlanStateType; + +typedef struct JsonTablePlanState +{ + JsonTablePlanStateType type; + + struct JsonTablePlanState *parent; + struct JsonTablePlanState *nested; +} JsonTablePlanState; + +typedef struct JsonTableScanState +{ + JsonTablePlanState plan; + + MemoryContext mcxt; + JsonPath *path; + List *args; + JsonValueList found; + JsonValueListIterator iter; + Datum current; + int ordinal; + bool currentIsNull; + bool outerJoin; + bool errorOnError; + bool advanceNested; + bool reset; +} JsonTableScanState; + +typedef struct JsonTableJoinState +{ + JsonTablePlanState plan; + + JsonTablePlanState *left; + JsonTablePlanState *right; + bool cross; + bool advanceRight; +} JsonTableJoinState; + +/* random number to identify JsonTableExecContext */ +#define JSON_TABLE_EXEC_CONTEXT_MAGIC 418352867 + +typedef struct JsonTableExecContext +{ + int magic; + JsonTableScanState **colexprscans; + JsonTableScanState *root; + bool empty; +} JsonTableExecContext; + /* strict/lax flags is decomposed into four [un]wrap/error flags */ #define jspStrictAbsenseOfErrors(cxt) (!(cxt)->laxMode) #define jspAutoUnwrap(cxt) ((cxt)->laxMode) @@ -248,6 +307,7 @@ static JsonPathExecResult getArrayIndex(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbValue *jb, int32 *index); static JsonBaseObjectInfo setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id); +static void JsonValueListClear(JsonValueList *jvl); static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv); static int JsonValueListLength(const JsonValueList *jvl); static bool JsonValueListIsEmpty(JsonValueList *jvl); @@ -265,6 +325,14 @@ static JsonbValue *wrapItemsInArray(const JsonValueList *items); static int compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, bool useTz, bool *cast_error); + +static JsonTablePlanState *JsonTableInitPlanState(JsonTableExecContext *cxt, + Node *plan, + JsonTablePlanState *parent); +static bool JsonTablePlanNextRow(JsonTablePlanState *state); +static bool JsonTableScanNextRow(JsonTableScanState *scan); + + /****************** User interface to JsonPath executor ********************/ /* @@ -2518,6 +2586,13 @@ setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id) return baseObject; } +static void +JsonValueListClear(JsonValueList *jvl) +{ + jvl->singleton = NULL; + jvl->list = NULL; +} + static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv) { @@ -3123,3 +3198,471 @@ JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); } } + +/************************ JSON_TABLE functions ***************************/ + +/* + * Returns private data from executor state. Ensure validity by check with + * MAGIC number. + */ +static inline JsonTableExecContext * +GetJsonTableExecContext(TableFuncScanState *state, const char *fname) +{ + JsonTableExecContext *result; + + if (!IsA(state, TableFuncScanState)) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + result = (JsonTableExecContext *) state->opaque; + if (result->magic != JSON_TABLE_EXEC_CONTEXT_MAGIC) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + + return result; +} + +/* Recursively initialize JSON_TABLE scan / join state */ +static JsonTableJoinState * +JsonTableInitJoinState(JsonTableExecContext *cxt, JsonTableSibling *plan, + JsonTablePlanState *parent) +{ + JsonTableJoinState *join = palloc0(sizeof(*join)); + + join->plan.type = JSON_TABLE_JOIN_STATE; + /* parent and nested not set. */ + + join->cross = plan->cross; + join->left = JsonTableInitPlanState(cxt, plan->larg, parent); + join->right = JsonTableInitPlanState(cxt, plan->rarg, parent); + + return join; +} + +static JsonTableScanState * +JsonTableInitScanState(JsonTableExecContext *cxt, JsonTableParent *plan, + JsonTablePlanState *parent, + List *args, MemoryContext mcxt) +{ + JsonTableScanState *scan = palloc0(sizeof(*scan)); + int i; + + scan->plan.type = JSON_TABLE_SCAN_STATE; + scan->plan.parent = parent; + + scan->outerJoin = plan->outerJoin; + scan->errorOnError = plan->errorOnError; + scan->path = DatumGetJsonPathP(plan->path->value->constvalue); + scan->args = args; + scan->mcxt = AllocSetContextCreate(mcxt, "JsonTableExecContext", + ALLOCSET_DEFAULT_SIZES); + + /* + * Set after settings scan->args and scan->mcxt, because the recursive call + * wants to use those values. + */ + scan->plan.nested = plan->child ? + JsonTableInitPlanState(cxt, plan->child, (JsonTablePlanState *) scan) : + NULL; + + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + + for (i = plan->colMin; i <= plan->colMax; i++) + cxt->colexprscans[i] = scan; + + return scan; +} + +/* Recursively initialize JSON_TABLE scan state */ +static JsonTablePlanState * +JsonTableInitPlanState(JsonTableExecContext *cxt, Node *plan, + JsonTablePlanState *parent) +{ + JsonTablePlanState *state; + + if (IsA(plan, JsonTableSibling)) + { + JsonTableSibling *join = castNode(JsonTableSibling, plan); + + state = (JsonTablePlanState *) + JsonTableInitJoinState(cxt, join, parent); + } + else + { + JsonTableParent *scan = castNode(JsonTableParent, plan); + JsonTableScanState *parent_scan = (JsonTableScanState *) parent; + + Assert(parent_scan); + state = (JsonTablePlanState *) + JsonTableInitScanState(cxt, scan, parent, parent_scan->args, + parent_scan->mcxt); + } + + return state; +} + +/* + * JsonTableInitOpaque + * Fill in TableFuncScanState->opaque for JsonTable processor + */ +static void +JsonTableInitOpaque(TableFuncScanState *state, int natts) +{ + JsonTableExecContext *cxt; + PlanState *ps = &state->ss.ps; + TableFuncScan *tfs = castNode(TableFuncScan, ps->plan); + TableFunc *tf = tfs->tablefunc; + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + JsonExpr *je = castNode(JsonExpr, tf->docexpr); + List *args = NIL; + + cxt = palloc0(sizeof(JsonTableExecContext)); + cxt->magic = JSON_TABLE_EXEC_CONTEXT_MAGIC; + + if (state->passingvalexprs) + { + ListCell *exprlc; + ListCell *namelc; + + Assert(list_length(state->passingvalexprs) == + list_length(je->passing_names)); + forboth(exprlc, state->passingvalexprs, + namelc, je->passing_names) + { + ExprState *state = lfirst_node(ExprState, exprlc); + String *name = lfirst_node(String, namelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(name->sval); + var->typid = exprType((Node *) state->expr); + var->typmod = exprTypmod((Node *) state->expr); + + /* + * Evaluate the expression and save the value to be returned by + * GetJsonPathVar(). + */ + var->value = ExecEvalExpr(state, ps->ps_ExprContext, + &var->isnull); + + args = lappend(args, var); + } + } + + cxt->colexprscans = palloc(sizeof(JsonTableScanState *) * + list_length(tf->colvalexprs)); + + cxt->root = JsonTableInitScanState(cxt, root, NULL, args, + CurrentMemoryContext); + state->opaque = cxt; +} + +/* Reset scan iterator to the beginning of the item list */ +static void +JsonTableRescan(JsonTableScanState *scan) +{ + JsonValueListInitIterator(&scan->found, &scan->iter); + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + scan->advanceNested = false; + scan->ordinal = 0; +} + +/* Reset context item of a scan, execute JSON path and reset a scan */ +static void +JsonTableResetContextItem(JsonTableScanState *scan, Datum item) +{ + MemoryContext oldcxt; + JsonPathExecResult res; + Jsonb *js = (Jsonb *) DatumGetJsonbP(item); + + JsonValueListClear(&scan->found); + + MemoryContextResetOnly(scan->mcxt); + + oldcxt = MemoryContextSwitchTo(scan->mcxt); + + res = executeJsonPath(scan->path, scan->args, GetJsonPathVar, js, + scan->errorOnError, &scan->found, + false /* FIXME */ ); + + MemoryContextSwitchTo(oldcxt); + + if (jperIsError(res)) + { + Assert(!scan->errorOnError); + JsonValueListClear(&scan->found); /* EMPTY ON ERROR case */ + } + + JsonTableRescan(scan); +} + +/* + * JsonTableSetDocument + * Install the input document + */ +static void +JsonTableSetDocument(TableFuncScanState *state, Datum value) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableSetDocument"); + + JsonTableResetContextItem(cxt->root, value); +} + +/* Recursively reset scan and its child nodes */ +static void +JsonTableRescanRecursive(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTableRescanRecursive(join->left); + JsonTableRescanRecursive(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan = (JsonTableScanState *) state; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + JsonTableRescan(scan); + if (scan->plan.nested) + JsonTableRescanRecursive(scan->plan.nested); + } +} + +/* + * Fetch next row from a cross/union joined scan. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTablePlanNextRow(JsonTablePlanState *state) +{ + JsonTableJoinState *join; + + if (state->type == JSON_TABLE_SCAN_STATE) + return JsonTableScanNextRow((JsonTableScanState *) state); + + join = (JsonTableJoinState *) state; + if (join->advanceRight) + { + /* fetch next inner row */ + if (JsonTablePlanNextRow(join->right)) + return true; + + /* inner rows are exhausted */ + if (join->cross) + join->advanceRight = false; /* next outer row */ + else + return false; /* end of scan */ + } + + while (!join->advanceRight) + { + /* fetch next outer row */ + bool left = JsonTablePlanNextRow(join->left); + + if (join->cross) + { + if (!left) + return false; /* end of scan */ + + JsonTableRescanRecursive(join->right); + + if (!JsonTablePlanNextRow(join->right)) + continue; /* next outer row */ + + join->advanceRight = true; /* next inner row */ + } + else if (!left) + { + if (!JsonTablePlanNextRow(join->right)) + return false; /* end of scan */ + + join->advanceRight = true; /* next inner row */ + } + + break; + } + + return true; +} + +/* Recursively set 'reset' flag of scan and its child nodes */ +static void +JsonTablePlanReset(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTablePlanReset(join->left); + JsonTablePlanReset(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + scan = (JsonTableScanState *) state; + scan->reset = true; + scan->advanceNested = false; + + if (scan->plan.nested) + JsonTablePlanReset(scan->plan.nested); + } +} + +/* + * Fetch next row from a simple scan with outer/inner joined nested subscans. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTableScanNextRow(JsonTableScanState *scan) +{ + /* reset context item if requested */ + if (scan->reset) + { + JsonTableScanState *parent_scan = + (JsonTableScanState *) scan->plan.parent; + + Assert(parent_scan && !parent_scan->currentIsNull); + JsonTableResetContextItem(scan, parent_scan->current); + scan->reset = false; + } + + if (scan->advanceNested) + { + /* fetch next nested row */ + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested) + return true; + } + + for (;;) + { + /* fetch next row */ + JsonbValue *jbv = JsonValueListNext(&scan->found, &scan->iter); + MemoryContext oldcxt; + + if (!jbv) + { + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + return false; /* end of scan */ + } + + /* set current row item */ + oldcxt = MemoryContextSwitchTo(scan->mcxt); + scan->current = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + scan->currentIsNull = false; + MemoryContextSwitchTo(oldcxt); + + scan->ordinal++; + + if (!scan->plan.nested) + break; + + JsonTablePlanReset(scan->plan.nested); + + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested || scan->outerJoin) + break; + } + + return true; +} + +/* + * JsonTableFetchRow + * Prepare the next "current" tuple for upcoming GetValue calls. + * Returns FALSE if the row-filter expression returned no more rows. + */ +static bool +JsonTableFetchRow(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableFetchRow"); + + if (cxt->empty) + return false; + + return JsonTableScanNextRow(cxt->root); +} + +/* + * JsonTableGetValue + * Return the value for column number 'colnum' for the current row. + * + * This leaks memory, so be sure to reset often the context in which it's + * called. + */ +static Datum +JsonTableGetValue(TableFuncScanState *state, int colnum, + Oid typid, int32 typmod, bool *isnull) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableGetValue"); + ExprContext *econtext = state->ss.ps.ps_ExprContext; + ExprState *estate = list_nth(state->colvalexprs, colnum); + JsonTableScanState *scan = cxt->colexprscans[colnum]; + Datum result; + + if (scan->currentIsNull) /* NULL from outer/union join */ + { + result = (Datum) 0; + *isnull = true; + } + else if (estate) /* regular column */ + { + Datum saved_caseValue = econtext->caseValue_datum; + bool saved_caseIsNull = econtext->caseValue_isNull; + + /* Pass the value for CaseTestExpr that may be present in colexpr */ + econtext->caseValue_datum = scan->current; + econtext->caseValue_isNull = false; + + result = ExecEvalExpr(estate, econtext, isnull); + + econtext->caseValue_datum = saved_caseValue; + econtext->caseValue_isNull = saved_caseIsNull; + } + else + { + result = Int32GetDatum(scan->ordinal); /* ordinality column */ + *isnull = false; + } + + return result; +} + +/* + * JsonTableDestroyOpaque + */ +static void +JsonTableDestroyOpaque(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableDestroyOpaque"); + + /* not valid anymore */ + cxt->magic = 0; + + state->opaque = NULL; +} + +const TableFuncRoutine JsonbTableRoutine = +{ + JsonTableInitOpaque, + JsonTableSetDocument, + NULL, + NULL, + NULL, + JsonTableFetchRow, + JsonTableGetValue, + JsonTableDestroyOpaque +}; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d1ec418ccf..e89d1e03d6 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -522,6 +522,8 @@ static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); static void get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit); +static void get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8606,7 +8608,8 @@ get_json_behavior(JsonBehavior *behavior, deparse_context *context, /* * get_json_expr_options * - * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS and + * JSON_TABLE columns. */ static void get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, @@ -9854,6 +9857,9 @@ get_rule_expr(Node *node, deparse_context *context, case JSON_EXISTS_OP: appendStringInfoString(buf, "JSON_EXISTS("); break; + default: + elog(ERROR, "unexpected JsonExpr type: %d", jexpr->op); + break; } get_rule_expr(jexpr->formatted_expr, context, showimplicit); @@ -11207,16 +11213,14 @@ get_sublink_expr(SubLink *sublink, deparse_context *context) /* ---------- - * get_tablefunc - Parse back a table function + * get_xmltable - Parse back a XMLTABLE function * ---------- */ static void -get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +get_xmltable(TableFunc *tf, deparse_context *context, bool showimplicit) { StringInfo buf = context->buf; - /* XMLTABLE is the only existing implementation. */ - appendStringInfoString(buf, "XMLTABLE("); if (tf->ns_uris != NIL) @@ -11307,6 +11311,271 @@ get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) appendStringInfoChar(buf, ')'); } +/* + * get_json_nested_columns - Parse back nested JSON_TABLE columns + */ +static void +get_json_table_nested_columns(TableFunc *tf, Node *node, + deparse_context *context, bool showimplicit, + bool needcomma) +{ + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_nested_columns(tf, n->larg, context, showimplicit, + needcomma); + get_json_table_nested_columns(tf, n->rarg, context, showimplicit, true); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + if (needcomma) + appendStringInfoChar(context->buf, ','); + + appendStringInfoChar(context->buf, ' '); + appendContextKeyword(context, "NESTED PATH ", 0, 0, 0); + get_const_expr(n->path->value, context, -1); + appendStringInfo(context->buf, " AS %s", quote_identifier(n->path->name)); + get_json_table_columns(tf, n, context, showimplicit); + } +} + +/* + * get_json_table_plan - Parse back a JSON_TABLE plan + */ +static void +get_json_table_plan(TableFunc *tf, Node *node, deparse_context *context, + bool parenthesize) +{ + if (parenthesize) + appendStringInfoChar(context->buf, '('); + + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_plan(tf, n->larg, context, + IsA(n->larg, JsonTableSibling) || + castNode(JsonTableParent, n->larg)->child); + + appendStringInfoString(context->buf, n->cross ? " CROSS " : " UNION "); + + get_json_table_plan(tf, n->rarg, context, + IsA(n->rarg, JsonTableSibling) || + castNode(JsonTableParent, n->rarg)->child); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + appendStringInfoString(context->buf, quote_identifier(n->path->name)); + + if (n->child) + { + appendStringInfoString(context->buf, + n->outerJoin ? " OUTER " : " INNER "); + get_json_table_plan(tf, n->child, context, + IsA(n->child, JsonTableSibling)); + } + } + + if (parenthesize) + appendStringInfoChar(context->buf, ')'); +} + +/* + * get_json_table_columns - Parse back JSON_TABLE columns + */ +static void +get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + ListCell *lc_colname; + ListCell *lc_coltype; + ListCell *lc_coltypmod; + ListCell *lc_colvalexpr; + int colnum = 0; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "COLUMNS (", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forfour(lc_colname, tf->colnames, + lc_coltype, tf->coltypes, + lc_coltypmod, tf->coltypmods, + lc_colvalexpr, tf->colvalexprs) + { + char *colname = strVal(lfirst(lc_colname)); + JsonExpr *colexpr; + Oid typid; + int32 typmod; + bool ordinality; + JsonBehaviorType default_behavior; + + typid = lfirst_oid(lc_coltype); + typmod = lfirst_int(lc_coltypmod); + colexpr = castNode(JsonExpr, lfirst(lc_colvalexpr)); + + if (colnum < node->colMin) + { + colnum++; + continue; + } + + if (colnum > node->colMax) + break; + + if (colnum > node->colMin) + appendStringInfoString(buf, ", "); + + colnum++; + + ordinality = !colexpr; + + appendContextKeyword(context, "", 0, 0, 0); + + appendStringInfo(buf, "%s %s", quote_identifier(colname), + ordinality ? "FOR ORDINALITY" : + format_type_with_typemod(typid, typmod)); + if (ordinality) + continue; + + if (colexpr->op == JSON_EXISTS_OP) + { + appendStringInfoString(buf, " EXISTS"); + default_behavior = JSON_BEHAVIOR_FALSE; + } + else + { + if (colexpr->op == JSON_QUERY_OP) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(typid, &typcategory, &typispreferred); + + if (typcategory == TYPCATEGORY_STRING) + appendStringInfoString(buf, + colexpr->format->format_type == JS_FORMAT_JSONB ? + " FORMAT JSONB" : " FORMAT JSON"); + } + + default_behavior = JSON_BEHAVIOR_NULL; + } + + if (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR) + default_behavior = JSON_BEHAVIOR_ERROR; + + appendStringInfoString(buf, " PATH "); + + get_json_path_spec(colexpr->path_spec, context, showimplicit); + + get_json_expr_options(colexpr, context, default_behavior); + } + + if (node->child) + get_json_table_nested_columns(tf, node->child, context, showimplicit, + node->colMax >= node->colMin); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_json_table - Parse back a JSON_TABLE function + * ---------- + */ +static void +get_json_table(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + + appendStringInfoString(buf, "JSON_TABLE("); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_const_expr(root->path->value, context, -1); + + appendStringInfo(buf, " AS %s", quote_identifier(root->path->name)); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PASSING ", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr((Node *) lfirst(lc2), context, false); + appendStringInfo(buf, " AS %s", + quote_identifier((lfirst_node(String, lc1))->sval) + ); + } + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + } + + get_json_table_columns(tf, root, context, showimplicit); + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PLAN ", 0, 0, 0); + get_json_table_plan(tf, (Node *) root, context, true); + + if (jexpr->on_error->btype != JSON_BEHAVIOR_EMPTY) + get_json_behavior(jexpr->on_error, context, "ERROR"); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_tablefunc - Parse back a table function + * ---------- + */ +static void +get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + /* XMLTABLE and JSON_TABLE are the only existing implementations. */ + + if (tf->functype == TFT_XMLTABLE) + get_xmltable(tf, context, showimplicit); + else if (tf->functype == TFT_JSON_TABLE) + get_json_table(tf, context, showimplicit); +} + /* ---------- * get_from_clause - Parse back a FROM clause * diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 584bf7001a..91453681e3 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1879,6 +1879,8 @@ typedef struct TableFuncScanState ExprState *rowexpr; /* state for row-generating expression */ List *colexprs; /* state for column-generating expression */ List *coldefexprs; /* state for column default expressions */ + List *colvalexprs; /* state for column value expression */ + List *passingvalexprs; /* state for PASSING argument expression */ List *ns_names; /* same as TableFunc.ns_names */ List *ns_uris; /* list of states of namespace URI exprs */ Bitmapset *notnulls; /* nullability flag for each output column */ diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 5e149b0266..d8466a2699 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -112,6 +112,8 @@ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *expr, JsonFormat *format); extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); +extern Node *makeJsonTableJoinedPlan(JsonTablePlanJoinType type, + Node *plan1, Node *plan2, int location); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 7d63a37d5f..b15f71cc92 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1733,6 +1733,19 @@ typedef enum JsonQuotes */ typedef char *JsonPathSpec; +/* + * JsonTableColumnType - + * enumeration of JSON_TABLE column types + */ +typedef enum JsonTableColumnType +{ + JTC_FOR_ORDINALITY, + JTC_REGULAR, + JTC_EXISTS, + JTC_FORMATTED, + JTC_NESTED, +} JsonTableColumnType; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1786,6 +1799,83 @@ typedef struct JsonFuncExpr int location; /* token location, or -1 if unknown */ } JsonFuncExpr; +/* + * JsonTableColumn - + * untransformed representation of JSON_TABLE column + */ +typedef struct JsonTableColumn +{ + NodeTag type; + JsonTableColumnType coltype; /* column type */ + char *name; /* column name */ + TypeName *typeName; /* column type name */ + char *pathspec; /* path specification, if any */ + char *pathname; /* path name, if any */ + JsonFormat *format; /* JSON format clause, if specified */ + JsonWrapper wrapper; /* WRAPPER behavior for formatted columns */ + bool omit_quotes; /* omit or keep quotes on scalar strings? */ + List *columns; /* nested columns */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + int location; /* token location, or -1 if unknown */ +} JsonTableColumn; + +/* + * JsonTablePlanType - + * flags for JSON_TABLE plan node types representation + */ +typedef enum JsonTablePlanType +{ + JSTP_DEFAULT, + JSTP_SIMPLE, + JSTP_JOINED, +} JsonTablePlanType; + +/* + * JsonTablePlanJoinType - + * flags for JSON_TABLE join types representation + */ +typedef enum JsonTablePlanJoinType +{ + JSTPJ_INNER = 0x01, + JSTPJ_OUTER = 0x02, + JSTPJ_CROSS = 0x04, + JSTPJ_UNION = 0x08, +} JsonTablePlanJoinType; + +typedef struct JsonTablePlan JsonTablePlan; + +/* + * JsonTablePlan - + * untransformed representation of JSON_TABLE plan node + */ +struct JsonTablePlan +{ + NodeTag type; + JsonTablePlanType plan_type; /* plan type */ + JsonTablePlanJoinType join_type; /* join type (for joined plan only) */ + JsonTablePlan *plan1; /* first joined plan */ + JsonTablePlan *plan2; /* second joined plan */ + char *pathname; /* path name (for simple plan only) */ + int location; /* token location, or -1 if unknown */ +}; + +/* + * JsonTable - + * untransformed representation of JSON_TABLE + */ +typedef struct JsonTable +{ + NodeTag type; + JsonCommon *common; /* common JSON path syntax fields */ + List *columns; /* list of JsonTableColumn */ + JsonTablePlan *plan; /* join plan, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + Alias *alias; /* table alias in FROM clause */ + bool lateral; /* does it have LATERAL prefix? */ + int location; /* token location, or -1 if unknown */ +} JsonTable; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 0e5a160c90..926ab12b9e 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -86,8 +86,14 @@ typedef struct RangeVar int location; } RangeVar; +typedef enum TableFuncType +{ + TFT_XMLTABLE, + TFT_JSON_TABLE +} TableFuncType; + /* - * TableFunc - node for a table function, such as XMLTABLE. + * TableFunc - node for a table function, such as XMLTABLE or JSON_TABLE. * * Entries in the ns_names list are either String nodes containing * literal namespace names, or NULL pointers to represent DEFAULT. @@ -95,6 +101,8 @@ typedef struct RangeVar typedef struct TableFunc { NodeTag type; + /* XMLTABLE or JSON_TABLE */ + TableFuncType functype; /* list of namespace URI expressions */ List *ns_uris pg_node_attr(query_jumble_ignore); /* list of namespace names or NULL */ @@ -115,8 +123,14 @@ typedef struct TableFunc List *colexprs; /* list of column default expressions */ List *coldefexprs pg_node_attr(query_jumble_ignore); + /* list of column value expressions */ + List *colvalexprs pg_node_attr(query_jumble_ignore); + /* list of PASSING argument expressions */ + List *passingvalexprs pg_node_attr(query_jumble_ignore); /* nullability flag for each output column */ Bitmapset *notnulls pg_node_attr(query_jumble_ignore); + /* JSON_TABLE plan */ + Node *plan pg_node_attr(query_jumble_ignore); /* counts from 0; -1 if none specified */ int ordinalitycol pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ @@ -1550,7 +1564,8 @@ typedef enum JsonExprOp { JSON_VALUE_OP, /* JSON_VALUE() */ JSON_QUERY_OP, /* JSON_QUERY() */ - JSON_EXISTS_OP /* JSON_EXISTS() */ + JSON_EXISTS_OP, /* JSON_EXISTS() */ + JSON_TABLE_OP /* JSON_TABLE() */ } JsonExprOp; /* @@ -1765,6 +1780,48 @@ typedef struct JsonExpr int location; /* token location, or -1 if unknown */ } JsonExpr; +/* + * JsonTablePath + * A JSON path expression to be computed as part of evaluating + * a JSON_TABLE plan node + */ +typedef struct JsonTablePath +{ + NodeTag type; + + Const *value; + char *name; +} JsonTablePath; + +/* + * JsonTableParent - + * transformed representation of parent JSON_TABLE plan node + */ +typedef struct JsonTableParent +{ + NodeTag type; + JsonTablePath *path; + Node *child; /* nested columns, if any */ + bool outerJoin; /* outer or inner join for nested columns? */ + int colMin; /* min column index in the resulting column + * list */ + int colMax; /* max column index in the resulting column + * list */ + bool errorOnError; /* ERROR/EMPTY ON ERROR behavior */ +} JsonTableParent; + +/* + * JsonTableSibling - + * transformed representation of joined sibling JSON_TABLE plan node + */ +typedef struct JsonTableSibling +{ + NodeTag type; + Node *larg; /* left join node */ + Node *rarg; /* right join node */ + bool cross; /* cross or union join? */ +} JsonTableSibling; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0954d9fc7b..f88a6c9ac6 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -242,6 +242,7 @@ PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_table", JSON_TABLE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) @@ -284,6 +285,7 @@ PG_KEYWORD("names", NAMES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("national", NATIONAL, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("natural", NATURAL, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("nchar", NCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("nested", NESTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("new", NEW, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("next", NEXT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("nfc", NFC, UNRESERVED_KEYWORD, BARE_LABEL) @@ -334,7 +336,9 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h index 8e26482db4..7152a8ef48 100644 --- a/src/include/parser/parse_clause.h +++ b/src/include/parser/parse_clause.h @@ -51,4 +51,7 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle, extern Index assignSortGroupRef(TargetEntry *tle, List *tlist); extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList); +/* functions in parse_jsontable.c */ +extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt); + #endif /* PARSE_CLAUSE_H */ diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index 4dae78a98c..b7ada77cb1 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -15,6 +15,7 @@ #define JSONPATH_H #include "fmgr.h" +#include "executor/tablefunc.h" #include "nodes/pg_list.h" #include "nodes/primnodes.h" #include "utils/jsonb.h" @@ -288,4 +289,6 @@ extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars); +extern PGDLLIMPORT const TableFuncRoutine JsonbTableRoutine; + #endif diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out index 8b87580752..995f267404 100644 --- a/src/test/regress/expected/json_sqljson.out +++ b/src/test/regress/expected/json_sqljson.out @@ -16,3 +16,9 @@ ERROR: JSON_QUERY() is not yet implemented for the json type LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); ^ HINT: Try casting the argument to jsonb +-- JSON_TABLE +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); +ERROR: JSON_TABLE() is not yet implemented for the json type +LINE 1: SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo ... + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out index 22f8679917..4323ed6b35 100644 --- a/src/test/regress/expected/jsonb_sqljson.out +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -1040,3 +1040,1072 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.dateti ERROR: functions in index expression must be marked IMMUTABLE CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; +-- JSON_TABLE +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); +ERROR: syntax error at or near "(" +LINE 1: SELECT JSON_TABLE('[]', '$'); + ^ +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); +ERROR: syntax error at or near ")" +LINE 1: SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + ^ +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); +ERROR: JSON_TABLE function has 1 columns available but 2 columns specified +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + foo +----- +(0 rows) + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + item | foo +------+----- + 123 | +(1 row) + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + js | id | id2 | int | text | char(4) | bool | numeric | domain | js | jb | jst | jsc | jsv | jsb | jsbq | aaa | aaa1 | exists1 | exists2 | exists3 | exists4 | js2 | jsb2w | jsb2q | ia | ta | jba +---------------------------------------------------------------------------------------+----+-----+-----+---------+---------+------+---------+---------+--------------+--------------+--------------+------+------+--------------+--------------+-----+------+---------+---------+---------+---------+--------------+----------------+--------------+----+----+----- + 1 | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [] | | | | | | | | | | | | | | | | | | | | | | | | | | | + {} | 1 | 1 | | | | | | | {} | {} | {} | {} | {} | {} | {} | | | f | 0 | | false | {} | [{}] | {} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 2 | 2 | 1 | 1.23 | 1.23 | | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | | | f | 0 | | false | 1.23 | [1.23] | 1.23 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 3 | 3 | 2 | 2 | 2 | | 2 | 2 | "2" | "2" | "2" | "2" | "2" | "2" | 2 | | | f | 0 | | false | "2" | ["2"] | 2 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 4 | 4 | | aaaaaaa | aaaa | | | aaaaaaa | "aaaaaaa" | "aaaaaaa" | "aaaaaaa" | "aaa | "aaa | "aaaaaaa" | | | | f | 0 | | false | "aaaaaaa" | ["aaaaaaa"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 5 | 5 | | foo | foo | | | | "foo" | "foo" | "foo" | "foo | "foo | "foo" | | | | f | 0 | | false | "foo" | ["foo"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 6 | 6 | | | | | | | null | null | null | null | null | null | null | | | f | 0 | | false | null | [null] | null | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 7 | 7 | 0 | false | fals | f | | false | false | false | false | fals | fals | false | false | | | f | 0 | | false | false | [false] | false | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 8 | 8 | 1 | true | true | t | | true | true | true | true | true | true | true | true | | | f | 0 | | false | true | [true] | true | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 9 | 9 | | | | | | | {"aaa": 123} | {"aaa": 123} | {"aaa": 123} | {"aa | {"aa | {"aaa": 123} | {"aaa": 123} | 123 | 123 | t | 1 | 1 | true | {"aaa": 123} | [{"aaa": 123}] | {"aaa": 123} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 10 | 10 | | [1,2] | [1,2 | | | [1,2] | "[1,2]" | "[1,2]" | "[1,2]" | "[1, | "[1, | "[1,2]" | [1, 2] | | | f | 0 | | false | "[1,2]" | ["[1,2]"] | [1, 2] | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 11 | 11 | | "str" | "str | | | "str" | "\"str\"" | "\"str\"" | "\"str\"" | "\"s | "\"s | "\"str\"" | "str" | | | f | 0 | | false | "\"str\"" | ["\"str\""] | "str" | | | +(14 rows) + +-- JSON_TABLE: Test backward parsing +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); +\sv jsonb_table_view +CREATE OR REPLACE VIEW public.jsonb_table_view AS + SELECT id, + id2, + "int", + text, + "char(4)", + bool, + "numeric", + domain, + js, + jb, + jst, + jsc, + jsv, + jsb, + jsbq, + aaa, + aaa1, + exists1, + exists2, + exists3, + js2, + jsb2w, + jsb2q, + ia, + ta, + jba, + a1, + b1, + a11, + a21, + a22 + FROM JSON_TABLE( + 'null'::jsonb, '$[*]' AS json_table_path_1 + PASSING + 1 + 2 AS a, + '"foo"'::json AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, + "int" integer PATH '$', + text text PATH '$', + "char(4)" character(4) PATH '$', + bool boolean PATH '$', + "numeric" numeric PATH '$', + domain jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc character(4) FORMAT JSON PATH '$', + jsv character varying(4) FORMAT JSON PATH '$', + jsb jsonb PATH '$', + jsbq jsonb PATH '$' OMIT QUOTES, + aaa integer PATH '$."aaa"', + aaa1 integer PATH '$."aaa"', + exists1 boolean EXISTS PATH '$."aaa"', + exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia integer[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 + COLUMNS ( + a1 integer PATH '$."a1"', + b1 text PATH '$."b1"', + NESTED PATH '$[*]' AS "p1 1" + COLUMNS ( + a11 text PATH '$."a11"' + ) + ), + NESTED PATH '$[2]' AS p2 + COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" + COLUMNS ( + a21 text PATH '$."a21"' + ), + NESTED PATH '$[*]' AS p22 + COLUMNS ( + a22 text PATH '$."a22"' + ) + ) + ) + PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22)))) + ) +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Table Function Scan on "json_table" + Output: "json_table".id, "json_table".id2, "json_table"."int", "json_table".text, "json_table"."char(4)", "json_table".bool, "json_table"."numeric", "json_table".domain, "json_table".js, "json_table".jb, "json_table".jst, "json_table".jsc, "json_table".jsv, "json_table".jsb, "json_table".jsbq, "json_table".aaa, "json_table".aaa1, "json_table".exists1, "json_table".exists2, "json_table".exists3, "json_table".js2, "json_table".jsb2w, "json_table".jsb2q, "json_table".ia, "json_table".ta, "json_table".jba, "json_table".a1, "json_table".b1, "json_table".a11, "json_table".a21, "json_table".a22 + Table Function Call: JSON_TABLE('null'::jsonb, '$[*]' AS json_table_path_1 PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (id FOR ORDINALITY, id2 FOR ORDINALITY, "int" integer PATH '$', text text PATH '$', "char(4)" character(4) PATH '$', bool boolean PATH '$', "numeric" numeric PATH '$', domain jsonb_test_domain PATH '$', js json PATH '$', jb jsonb PATH '$', jst text FORMAT JSON PATH '$', jsc character(4) FORMAT JSON PATH '$', jsv character varying(4) FORMAT JSON PATH '$', jsb jsonb PATH '$', jsbq jsonb PATH '$' OMIT QUOTES, aaa integer PATH '$."aaa"', aaa1 integer PATH '$."aaa"', exists1 boolean EXISTS PATH '$."aaa"', exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, js2 json PATH '$', jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, jsb2q jsonb PATH '$' OMIT QUOTES, ia integer[] PATH '$', ta text[] PATH '$', jba jsonb[] PATH '$', NESTED PATH '$[1]' AS p1 COLUMNS (a1 integer PATH '$."a1"', b1 text PATH '$."b1"', NESTED PATH '$[*]' AS "p1 1" COLUMNS (a11 text PATH '$."a11"')), NESTED PATH '$[2]' AS p2 COLUMNS ( NESTED PATH '$[*]' AS "p2:1" COLUMNS (a21 text PATH '$."a21"'), NESTED PATH '$[*]' AS p22 COLUMNS (a22 text PATH '$."a22"'))) PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22))))) +(3 rows) + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + js | a +-------+--- + 1 | 1 + "err" | +(2 rows) + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; + a +--- + +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: jsonpath member accessor can only be applied to an object +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: no SQL/JSON item +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 1 +(1 row) + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); + a +--- + 0 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to smallint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to bigint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to real +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 E... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); + a +----- + fal +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to json +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to jsonb +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EX... + ^ +-- JSON_TABLE: nested paths and plans +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb '[]', '$' -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 4: NESTED PATH '$' COLUMNS ( -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: b +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +-- JSON_TABLE: plan validation +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p1) + ^ +DETAIL: Path name mismatch: expected p0 but p1 is given. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 UNION p1 UNION p11) + ^ +DETAIL: Expected INNER or OUTER JSON_TABLE plan node. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 8: NESTED PATH '$' AS p2 COLUMNS ( + ^ +DETAIL: Plan node for nested path p2 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 5: NESTED PATH '$' AS p11 COLUMNS ( foo int ), + ^ +DETAIL: Plan node for nested path p11 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) + ^ +DETAIL: Plan node contains some extra or duplicate sibling nodes. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 6: NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ^ +DETAIL: Plan node for nested path p12 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 9: NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ^ +DETAIL: Plan node for nested path p21 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + bar | foo | baz +-----+-----+----- +(0 rows) + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb 'null', 'strict $[*]' -- without root path name + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- JSON_TABLE: plan execution +CREATE TEMP TABLE jsonb_table_test (js jsonb); +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + n | a | c | b +---+----+----+--- + 1 | 1 | | + 2 | 2 | 10 | + 2 | 2 | | + 2 | 2 | 20 | + 2 | 2 | | 1 + 2 | 2 | | 2 + 2 | 2 | | 3 + 3 | 3 | | 1 + 3 | 3 | | 2 + 4 | -1 | | 1 + 4 | -1 | | 2 +(11 rows) + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + n | a | b | b1 | c | c1 | b +---+---+--------------+-----+------+----+----- + 1 | 1 | [1, 10] | 1 | 1 | | 101 + 1 | 1 | [1, 10] | 1 | null | | 101 + 1 | 1 | [1, 10] | 1 | 2 | | 101 + 1 | 1 | [1, 10] | 10 | 1 | | 110 + 1 | 1 | [1, 10] | 10 | null | | 110 + 1 | 1 | [1, 10] | 10 | 2 | | 110 + 1 | 1 | [2] | 2 | 1 | | 102 + 1 | 1 | [2] | 2 | null | | 102 + 1 | 1 | [2] | 2 | 2 | | 102 + 1 | 1 | [3, 30, 300] | 3 | 1 | | 103 + 1 | 1 | [3, 30, 300] | 3 | null | | 103 + 1 | 1 | [3, 30, 300] | 3 | 2 | | 103 + 1 | 1 | [3, 30, 300] | 30 | 1 | | 130 + 1 | 1 | [3, 30, 300] | 30 | null | | 130 + 1 | 1 | [3, 30, 300] | 30 | 2 | | 130 + 1 | 1 | [3, 30, 300] | 300 | 1 | | 400 + 1 | 1 | [3, 30, 300] | 300 | null | | 400 + 1 | 1 | [3, 30, 300] | 300 | 2 | | 400 + 2 | 2 | | | | | + 3 | | | | | | +(20 rows) + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + x | y | y | z +---+---+--------------+--- + 2 | 1 | [1, 2, 3] | 1 + 2 | 1 | [1, 2, 3] | 2 + 2 | 1 | [1, 2, 3] | 3 + 3 | 1 | [1, 2, 3] | 1 + 3 | 1 | [1, 2, 3] | 2 + 3 | 1 | [1, 2, 3] | 3 + 3 | 1 | [2, 3, 4, 5] | 2 + 3 | 1 | [2, 3, 4, 5] | 3 + 3 | 1 | [2, 3, 4, 5] | 4 + 3 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [1, 2, 3] | 1 + 4 | 1 | [1, 2, 3] | 2 + 4 | 1 | [1, 2, 3] | 3 + 4 | 1 | [2, 3, 4, 5] | 2 + 4 | 1 | [2, 3, 4, 5] | 3 + 4 | 1 | [2, 3, 4, 5] | 4 + 4 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [3, 4, 5, 6] | 3 + 4 | 1 | [3, 4, 5, 6] | 4 + 4 | 1 | [3, 4, 5, 6] | 5 + 4 | 1 | [3, 4, 5, 6] | 6 + 2 | 2 | [1, 2, 3] | 2 + 2 | 2 | [1, 2, 3] | 3 + 3 | 2 | [1, 2, 3] | 2 + 3 | 2 | [1, 2, 3] | 3 + 3 | 2 | [2, 3, 4, 5] | 2 + 3 | 2 | [2, 3, 4, 5] | 3 + 3 | 2 | [2, 3, 4, 5] | 4 + 3 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [1, 2, 3] | 2 + 4 | 2 | [1, 2, 3] | 3 + 4 | 2 | [2, 3, 4, 5] | 2 + 4 | 2 | [2, 3, 4, 5] | 3 + 4 | 2 | [2, 3, 4, 5] | 4 + 4 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [3, 4, 5, 6] | 3 + 4 | 2 | [3, 4, 5, 6] | 4 + 4 | 2 | [3, 4, 5, 6] | 5 + 4 | 2 | [3, 4, 5, 6] | 6 + 2 | 3 | [1, 2, 3] | 3 + 3 | 3 | [1, 2, 3] | 3 + 3 | 3 | [2, 3, 4, 5] | 3 + 3 | 3 | [2, 3, 4, 5] | 4 + 3 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [1, 2, 3] | 3 + 4 | 3 | [2, 3, 4, 5] | 3 + 4 | 3 | [2, 3, 4, 5] | 4 + 4 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [3, 4, 5, 6] | 3 + 4 | 3 | [3, 4, 5, 6] | 4 + 4 | 3 | [3, 4, 5, 6] | 5 + 4 | 3 | [3, 4, 5, 6] | 6 +(52 rows) + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; +ERROR: could not find jsonpath variable "x" +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); + json_value +------------ + foo +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_query +------------ + 123 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); + json_query +------------ + [123] +(1 row) + +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +ERROR: syntax error at or near " " of jsonpath input +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); +ERROR: only string constants supported in JSON_TABLE path specification +LINE 1: SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || '... + ^ diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql index 4f30fa46b9..df4a430d88 100644 --- a/src/test/regress/sql/json_sqljson.sql +++ b/src/test/regress/sql/json_sqljson.sql @@ -9,3 +9,7 @@ SELECT JSON_VALUE(NULL FORMAT JSON, '$'); -- JSON_QUERY SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + +-- JSON_TABLE + +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql index b8a6148b7b..fbd86f2084 100644 --- a/src/test/regress/sql/jsonb_sqljson.sql +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -325,3 +325,632 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; + +-- JSON_TABLE + +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); + +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); + +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); + +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + +-- JSON_TABLE: Test backward parsing + +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); + +\sv jsonb_table_view + +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; + +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); + +-- JSON_TABLE: nested paths and plans + +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; + +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; + +-- JSON_TABLE: plan validation + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; + +-- JSON_TABLE: plan execution + +CREATE TEMP TABLE jsonb_table_test (js jsonb); + +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); + +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; + +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 336b7faa36..8c0cca7c51 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1303,6 +1303,7 @@ JsonPathKeyword JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback +JsonPathSpec JsonPathString JsonPathVarCallback JsonPathVariable @@ -1311,6 +1312,17 @@ JsonQuotes JsonReturning JsonSemAction JsonSerializeExpr +JsonTable +JsonTableColumn +JsonTableColumnType +JsonTableContext +JsonTableJoinState +JsonTableParent +JsonTablePlan +JsonTablePlanJoinType +JsonTablePlanType +JsonTableScanState +JsonTableSibling JsonTokenType JsonTransformStringValuesAction JsonTypeCategory @@ -2766,6 +2778,7 @@ TableFunc TableFuncRoutine TableFuncScan TableFuncScanState +TableFuncType TableInfo TableLikeClause TableSampleClause -- 2.35.3
v3-0004-SQL-JSON-query-functions.patch (application/octet-stream, 210.3K) download From 6f9b166848d82ca9cb4b5f92b0fdb6c475981407 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Fri, 16 Jun 2023 17:49:18 +0900 Subject: [PATCH v3 4/6] SQL/JSON query functions This introduces the SQL/JSON functions for querying JSON data using jsonpath expressions. The functions are: JSON_EXISTS() JSON_QUERY() JSON_VALUE() All of these functions only operate on jsonb. The workaround for now is to cast the argument to jsonb. JSON_EXISTS() tests if the jsonpath expression applied to the jsonb value yields any values. JSON_VALUE() must return a single value, and an error occurs if it tries to return multiple values. JSON_QUERY() must return a json object or array, and there are various WRAPPER options for handling scalar or multi-value results. Both these functions have options for handling EMPTY and ERROR conditions. Nikita Glukhov Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby. Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org --- doc/src/sgml/func.sgml | 147 +++ src/backend/executor/execExpr.c | 432 ++++++++ src/backend/executor/execExprInterp.c | 502 ++++++++- src/backend/jit/llvm/llvmjit_expr.c | 250 +++++ src/backend/jit/llvm/llvmjit_types.c | 5 + src/backend/nodes/makefuncs.c | 15 + src/backend/nodes/nodeFuncs.c | 183 ++++ src/backend/optimizer/path/costsize.c | 3 +- src/backend/optimizer/util/clauses.c | 19 + src/backend/parser/gram.y | 348 ++++++- src/backend/parser/parse_collate.c | 7 + src/backend/parser/parse_expr.c | 559 +++++++++- src/backend/parser/parse_target.c | 15 + src/backend/utils/adt/formatting.c | 44 + src/backend/utils/adt/jsonb.c | 62 ++ src/backend/utils/adt/jsonfuncs.c | 170 ++- src/backend/utils/adt/jsonpath.c | 255 +++++ src/backend/utils/adt/jsonpath_exec.c | 391 ++++++- src/backend/utils/adt/ruleutils.c | 136 +++ src/include/executor/execExpr.h | 172 +++ src/include/executor/executor.h | 1 + src/include/nodes/execnodes.h | 3 + src/include/nodes/makefuncs.h | 1 + src/include/nodes/parsenodes.h | 48 + src/include/nodes/primnodes.h | 109 ++ src/include/parser/kwlist.h | 11 + src/include/utils/formatting.h | 2 +- src/include/utils/jsonb.h | 3 + src/include/utils/jsonfuncs.h | 6 + src/include/utils/jsonpath.h | 27 + src/interfaces/ecpg/preproc/ecpg.trailer | 28 + src/test/regress/expected/json_sqljson.out | 18 + src/test/regress/expected/jsonb_sqljson.out | 1042 +++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/json_sqljson.sql | 11 + src/test/regress/sql/jsonb_sqljson.sql | 327 ++++++ src/tools/pgindent/typedefs.list | 14 + 37 files changed, 5266 insertions(+), 102 deletions(-) create mode 100644 src/test/regress/expected/json_sqljson.out create mode 100644 src/test/regress/expected/jsonb_sqljson.out create mode 100644 src/test/regress/sql/json_sqljson.sql create mode 100644 src/test/regress/sql/jsonb_sqljson.sql diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 9cece06c18..cb36e1463b 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16995,6 +16995,153 @@ array w/o UK? | t </tbody> </tgroup> </table> + + <para> + <xref linkend="functions-sqljson-querying"/> details the SQL/JSON + functions that can be used to query JSON data. + </para> + + <note> + <para> + SQL/JSON paths can only be applied to the <type>jsonb</type> type, so it + might be necessary to cast the <replaceable>context_item</replaceable> + argument of these functions to <type>jsonb</type>. + </para> + </note> + + <table id="functions-sqljson-querying"> + <title>SQL/JSON Query Functions</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="func_table_entry"><para role="func_signature"> + Function signature + </para> + <para> + Description + </para> + <para> + Example(s) + </para></entry> + </row> + </thead> + <tbody> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_exists</primary></indexterm> + <function>json_exists</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>TRUE</literal> | <literal>FALSE</literal> |<literal> UNKNOWN</literal> | <literal>ERROR</literal> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns true if the SQL/JSON <replaceable>path_expression</replaceable> + applied to the <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s yields any items. + The <literal>ON ERROR</literal> clause specifies what is returned if + an error occurs. Note that if the <replaceable>path_expression</replaceable> + is <literal>strict</literal>, an error is generated if it yields no items. + The default value is <literal>UNKNOWN</literal> which causes a NULL + result. + </para> + <para> + <literal>json_exists(jsonb '{"key1": [1,2,3]}', 'strict $.key1[*] ? (@ > 2)')</literal> + <returnvalue>t</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'lax $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>f</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'strict $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>ERROR: jsonpath array subscript is out of bounds</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_value</primary></indexterm> + <function>json_value</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> + <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. The extracted value must be + a single <acronym>SQL/JSON</acronym> scalar item. For results that + are objects or arrays, use the <function>json_query</function> + function instead. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + The <literal>ON EMPTY</literal> clause specifies the behavior if the + <replaceable>path_expression</replaceable> yields no value at all. + The <literal>ON ERROR</literal> clause specifies the behavior if an + error occurs as a result of <type>jsonpath</type> evaluation + (including cast to the output type) or execution of + <literal>ON EMPTY</literal> behavior (that was caused by empty result + of <type>jsonpath</type> evaluation). + </para> + <para> + <literal>json_value(jsonb '"123.45"', '$' RETURNING float)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_value(jsonb '"03:04 2015-02-01"', '$.datetime("HH24:MI&nbsp;YYYY-MM-DD")' RETURNING date)</literal> + <returnvalue>2015-02-01</returnvalue> + </para> + <para> + <literal>json_value(jsonb '[1,2]', 'strict $[*]' DEFAULT 9 ON ERROR)</literal> + <returnvalue>9</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_query</primary></indexterm> + <function>json_query</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional> + <optional> { <literal>WITHOUT</literal> | <literal>WITH</literal> { <literal>CONDITIONAL</literal> | <optional><literal>UNCONDITIONAL</literal></optional> } } <optional> <literal>ARRAY</literal> </optional> <literal>WRAPPER</literal> </optional> + <optional> { <literal>KEEP</literal> | <literal>OMIT</literal> } <literal>QUOTES</literal> <optional> <literal>ON SCALAR STRING</literal> </optional> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. + This function must return a JSON string, so if the path expression + returns multiple SQL/JSON items, you must wrap the result using the + <literal>WITH WRAPPER</literal> clause. If the wrapper is + <literal>UNCONDITIONAL</literal>, an array wrapper will always + be applied, even if the returned value is already a single JSON object + or array, but if it is <literal>CONDITIONAL</literal>, it will not be + applied to a single array or object. <literal>UNCONDITIONAL</literal> + is the default. + If the result is a scalar string, by default the value returned will have + surrounding quotes making it a valid JSON value. However, this behavior + is reversed if <literal>OMIT QUOTES</literal> is specified. + The <literal>ON ERROR</literal> and <literal>ON EMPTY</literal> + clauses have similar semantics to those clauses for + <function>json_value</function>. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + </para> + <para> + <literal>json_query(jsonb '[1,[2,3],null]', 'lax $[*][1]' WITH CONDITIONAL WRAPPER)</literal> + <returnvalue>[3]</returnvalue> + </para></entry> + </row> + </tbody> + </tgroup> + </table> + </sect2> <sect2 id="functions-sqljson-path"> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 95d9fbbc7c..dbf2e692ee 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -50,6 +50,7 @@ #include "utils/datum.h" #include "utils/json.h" #include "utils/jsonb.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -89,6 +90,16 @@ static void ExecBuildAggTransCall(ExprState *state, AggState *aggstate, FunctionCallInfo fcinfo, AggStatePerTrans pertrans, int transno, int setno, int setoff, bool ishash, bool nullcheck); +static void ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch); +static Datum GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null); +static JsonCoercionState *ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull); +static JsonItemCoercionsState *ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull); /* @@ -2428,6 +2439,14 @@ ExecInitExprRec(Expr *node, ExprState *state, break; } + case T_JsonExpr: + { + JsonExpr *jexpr = castNode(JsonExpr, node); + + ExecInitJsonExpr(jexpr, state, resv, resnull, &scratch); + break; + } + case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -4195,3 +4214,416 @@ ExecBuildParamSetEqual(TupleDesc desc, return state; } + +/* + * Push steps to evaluate a JsonExpr and its various subsidiary expressions. + */ +static void +ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch) +{ + JsonExprState *jsestate = palloc0(sizeof(JsonExprState)); + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + ListCell *argexprlc; + ListCell *argnamelc; + int skip_step_off = -1; + int passing_args_step_off = -1; + int coercion_step_off = -1; + int coercion_finish_step_off = -1; + int behavior_step_off = -1; + int onempty_expr_step_off = -1; + int onempty_jump_step_off = -1; + int onerror_expr_step_off = -1; + int onerror_jump_step_off = -1; + List *adjust_jumps = NIL; + ListCell *lc; + ExprEvalStep *as; + + jsestate->jsexpr = jexpr; + + /* + * Add steps to compute formatted_expr, pathspec, and PASSING arg + * expressions as things that must be evaluated *before* the actual JSON + * path expression. + */ + ExecInitExprRec((Expr *) jexpr->formatted_expr, state, + &pre_eval->formatted_expr.value, + &pre_eval->formatted_expr.isnull); + ExecInitExprRec((Expr *) jexpr->path_spec, state, + &pre_eval->pathspec.value, + &pre_eval->pathspec.isnull); + + /* + * Before pushing steps for PASSING args, push a step to decide whether to + * skip evaluating the args and the JSON path expression depending on + * whether either of formatted_expr and pathspec is NULL; see + * ExecEvalJsonExprSkip(). + */ + scratch->opcode = EEOP_JSONEXPR_SKIP; + scratch->d.jsonexpr_skip.jsestate = jsestate; + skip_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* PASSING args. */ + jsestate->pre_eval.args = NIL; + passing_args_step_off = state->steps_len; + forboth(argexprlc, jexpr->passing_values, + argnamelc, jexpr->passing_names) + { + Expr *argexpr = (Expr *) lfirst(argexprlc); + String *argname = lfirst_node(String, argnamelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(argname->sval); + var->typid = exprType((Node *) argexpr); + var->typmod = exprTypmod((Node *) argexpr); + + ExecInitExprRec((Expr *) argexpr, state, &var->value, &var->isnull); + + pre_eval->args = lappend(pre_eval->args, var); + } + + /* + * Step for the actual JSON path evaluation; see ExecEvalJson() and + * ExecEvalJsonExpr(). + */ + scratch->opcode = EEOP_JSONEXPR_PATH; + scratch->d.jsonexpr.jsestate = jsestate; + ExprEvalPushStep(state, scratch); + + /* + * Push steps to control the evaluation of expressions based on the result + * of JSON path evaluation. + */ + + /* + * Step to handle ON ERROR and ON EMPTY behavior; see + * ExecEvalJsonExprBehavior(). + */ + scratch->opcode = EEOP_JSONEXPR_BEHAVIOR; + scratch->d.jsonexpr_behavior.jsestate = jsestate; + behavior_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Step(s) to evaluate ON EMPTY expression */ + onempty_expr_step_off = state->steps_len; + if (jexpr->on_empty && + jexpr->on_empty->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_empty->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_empty->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_empty, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onempty_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + } + } + + /* Step(s) to evaluate ON ERROR expression */ + onerror_expr_step_off = state->steps_len; + if (jexpr->on_error && + jexpr->on_error->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_error->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_error->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_error, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onerror_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set later */ + ExprEvalPushStep(state, scratch); + } + } + + /* + * Step to handle applying coercion to the JSON item returned by + * EEOP_JSONEXPR_PATH or to the ON EMPTY/ERROR expression as + * EEOP_JSONEXPR_BEHAVIOR decides; see ExecEvalJsonExprCoercion(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Initialize coercion expression(s). */ + if (jexpr->result_coercion) + { + jsestate->result_jcstate = + ExecInitJsonCoercion(scratch, state, jexpr->result_coercion, + resv, resnull); + /* See the comment above. */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* computed later */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + if (jexpr->coercions) + { + /* + * ExecPrepareJsonItemCoercion() called by ExecEvalJsonExpr() chooses + * one for a given JSON item returned by JsonPathValue(). + */ + jsestate->item_jcstates = + ExecInitJsonItemCoercions(scratch, state, jexpr->coercions, + resv, resnull); + } + + /* + * And a step to clean up after the coercion step; see + * ExecEvalJsonExprCoercionFinish(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION_FINISH; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_finish_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* + * Adjust jump target addresses in various post-eval steps now that we + * have all the steps in place. + */ + + /* EEOP_JSONEXPR_SKIP */ + Assert(skip_step_off >= 0); + as = &state->steps[skip_step_off]; + as->d.jsonexpr_skip.jump_coercion = coercion_step_off; + as->d.jsonexpr_skip.jump_passing_args = passing_args_step_off; + + /* EEOP_JSONEXPR_BEHAVIOR */ + Assert(behavior_step_off >= 0); + as = &state->steps[behavior_step_off]; + as->d.jsonexpr_behavior.jump_onerror_expr = onerror_expr_step_off; + as->d.jsonexpr_behavior.jump_onempty_expr = onempty_expr_step_off; + as->d.jsonexpr_behavior.jump_coercion = coercion_step_off; + as->d.jsonexpr_behavior.jump_skip_coercion = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION */ + Assert(coercion_step_off >= 0); + as = &state->steps[coercion_step_off]; + as->d.jsonexpr_coercion.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION_FINISH */ + Assert(coercion_finish_step_off >= 0); + as = &state->steps[coercion_finish_step_off]; + as->d.jsonexpr_coercion_finish.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion_finish.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JUMP steps */ + + /* + * Ones after ON EMPTY and ON ERROR non-default expressions should jump to + * the coercion step. + */ + if (onempty_jump_step_off >= 0) + { + as = &state->steps[onempty_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + if (onerror_jump_step_off >= 0) + { + as = &state->steps[onerror_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + + /* The rest should jump to the end. */ + foreach(lc, adjust_jumps) + { + as = &state->steps[lfirst_int(lc)]; + as->d.jump.jumpdone = state->steps_len; + } + + /* + * Set RETURNING type's input function used by ExecEvalJsonExprCoercion(). + */ + if (jexpr->omit_quotes || + (jexpr->result_coercion && jexpr->result_coercion->via_io)) + { + Oid typinput; + FmgrInfo *finfo; + + /* lookup the result type's input function */ + getTypeInputInfo(jexpr->returning->typid, &typinput, + &jsestate->input.typioparam); + finfo = palloc0(sizeof(FmgrInfo)); + fmgr_info(typinput, finfo); + jsestate->input.finfo = finfo; + } +} + +/* + * Evaluate a JSON error/empty behavior result. + */ +static Datum +GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null) +{ + *is_null = false; + + switch (behavior->btype) + { + case JSON_BEHAVIOR_EMPTY_ARRAY: + return JsonbPGetDatum(JsonbMakeEmptyArray()); + + case JSON_BEHAVIOR_EMPTY_OBJECT: + return JsonbPGetDatum(JsonbMakeEmptyObject()); + + case JSON_BEHAVIOR_TRUE: + return BoolGetDatum(true); + + case JSON_BEHAVIOR_FALSE: + return BoolGetDatum(false); + + case JSON_BEHAVIOR_NULL: + case JSON_BEHAVIOR_UNKNOWN: + case JSON_BEHAVIOR_EMPTY: + *is_null = true; + return (Datum) 0; + + case JSON_BEHAVIOR_DEFAULT: + /* Always handled in the caller. */ + Assert(false); + return (Datum) 0; + + default: + elog(ERROR, "unrecognized SQL/JSON behavior %d", behavior->btype); + return (Datum) 0; + } +} + +/* Initialize one JsonCoercion for execution. */ +static JsonCoercionState * +ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull) +{ + JsonCoercionState *jcstate = palloc0(sizeof(JsonCoercionState)); + + jcstate->coercion = coercion; + if (coercion && coercion->expr) + { + Datum *save_innermost_caseval; + bool *save_innermost_casenull; + + jcstate->jump_eval_expr = state->steps_len; + + /* Push step(s) to compute cstate->coercion. */ + save_innermost_caseval = state->innermost_caseval; + save_innermost_casenull = state->innermost_casenull; + + state->innermost_caseval = resv; + state->innermost_casenull = resnull; + + ExecInitExprRec((Expr *) coercion->expr, state, resv, resnull); + + state->innermost_caseval = save_innermost_caseval; + state->innermost_casenull = save_innermost_casenull; + } + else + jcstate->jump_eval_expr = -1; + + return jcstate; +} + +/* + * Push steps to evaluate coercions from a given JsonItemCoercions, which + * contains all possible coercions that may need to be applied to JSON + * items coming from evaluating the parent JsonExpr. + */ +static JsonItemCoercionsState * +ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull) +{ + JsonCoercion **coercion; + JsonItemCoercionsState *item_jcstates = palloc0(sizeof(JsonItemCoercionsState)); + JsonCoercionState **item_jcstate; + ExprEvalStep *as; + List *adjust_jumps = NIL; + ListCell *lc; + + /* Push the steps of individual coercions. */ + for (coercion = &coercions->null, + item_jcstate = &item_jcstates->null; + coercion <= &coercions->composite; + coercion++, item_jcstate++) + { + *item_jcstate = ExecInitJsonCoercion(scratch, state, *coercion, + resv, resnull); + + /* Emit JUMP step to skip past other coercions' steps. */ + scratch->opcode = EEOP_JUMP; + + /* + * Remember JUMP step address to set the actual jump target address + * below. + */ + adjust_jumps = lappend_int(adjust_jumps, state->steps_len); + ExprEvalPushStep(state, scratch); + } + + foreach(lc, adjust_jumps) + { + int jump_step_id = lfirst_int(lc); + + as = &state->steps[jump_step_id]; + as->d.jump.jumpdone = state->steps_len; + } + + return item_jcstates; +} diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index b83dd1c666..4b0647153a 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -74,6 +74,7 @@ #include "utils/json.h" #include "utils/jsonb.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/timestamp.h" @@ -152,6 +153,9 @@ static TupleDesc get_cached_rowtype(Oid type_id, int32 typmod, bool *changed); static void ExecEvalRowNullInt(ExprState *state, ExprEvalStep *op, ExprContext *econtext, bool checkisnull); +static Datum ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate); /* fast-path evaluation functions */ static Datum ExecJustInnerVar(ExprState *state, ExprContext *econtext, bool *isnull); @@ -480,6 +484,11 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_XMLEXPR, &&CASE_EEOP_JSON_CONSTRUCTOR, &&CASE_EEOP_IS_JSON, + &&CASE_EEOP_JSONEXPR_SKIP, + &&CASE_EEOP_JSONEXPR_PATH, + &&CASE_EEOP_JSONEXPR_BEHAVIOR, + &&CASE_EEOP_JSONEXPR_COERCION, + &&CASE_EEOP_JSONEXPR_COERCION_FINISH, &&CASE_EEOP_AGGREF, &&CASE_EEOP_GROUPING_FUNC, &&CASE_EEOP_WINDOW_FUNC, @@ -1186,8 +1195,14 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) /* second and third arguments are already set up */ fcinfo_in->isnull = false; + + /* Pass down soft-error capture node if any. */ + fcinfo_in->context = state->escontext; + d = FunctionCallInvoke(fcinfo_in); *op->resvalue = d; + if (SOFT_ERROR_OCCURRED(state->escontext)) + *op->resnull = true; /* Should get null result if and only if str is NULL */ if (str == NULL) @@ -1195,7 +1210,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) Assert(*op->resnull); Assert(fcinfo_in->isnull); } - else + else if (!SOFT_ERROR_OCCURRED(state->escontext)) { Assert(!*op->resnull); Assert(!fcinfo_in->isnull); @@ -1543,6 +1558,38 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_JSONEXPR_PATH) + { + /* too complex for an inline implementation */ + ExecEvalJsonExpr(state, op, econtext); + EEO_NEXT(); + } + + EEO_CASE(EEOP_JSONEXPR_SKIP) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprSkip(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_BEHAVIOR) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprBehavior(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercion(state, op, econtext, + *op->resvalue, *op->resnull)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION_FINISH) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercionFinish(state, op)); + } + EEO_CASE(EEOP_AGGREF) { /* @@ -3745,7 +3792,7 @@ ExecEvalConstraintCheck(ExprState *state, ExprEvalStep *op) { if (!*op->d.domaincheck.checknull && !DatumGetBool(*op->d.domaincheck.checkvalue)) - ereport(ERROR, + errsave(state->escontext, (errcode(ERRCODE_CHECK_VIOLATION), errmsg("value for domain %s violates check constraint \"%s\"", format_type_be(op->d.domaincheck.resulttype), @@ -4137,6 +4184,457 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op) *op->resvalue = BoolGetDatum(res); } +/* + * Evaluate given JsonExpr by performing the specified JSON operation. + * + * This also populates the JsonExprPostEvalState with the information needed + * by the subsequent steps that handle the specified JsonBehavior. + */ +void +ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) +{ + JsonExprState *jsestate = op->d.jsonexpr.jsestate; + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonExpr *jexpr = jsestate->jsexpr; + Datum item; + Datum res = (Datum) 0; + bool resnull = true; + JsonPath *path; + bool *error; + bool *empty; + + item = pre_eval->formatted_expr.value; + path = DatumGetJsonPathP(pre_eval->pathspec.value); + + /* Reset JsonExprPostEvalState for this evaluation. */ + memset(post_eval, 0, sizeof(*post_eval)); + + /* Respect ON ERROR behavior during path evaluation. */ + jsestate->throw_error = (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR); + + /* + * Be sure to save the error (if needed) and empty statuses for the + * EEOP_JSONEXPR_BEHAVIOR step to peruse. + */ + error = !jsestate->throw_error ? &post_eval->error : NULL; + empty = &post_eval->empty; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + res = JsonPathQuery(item, path, jexpr->wrapper, empty, error, + pre_eval->args); + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + resnull = !DatumGetPointer(res); + break; + + case JSON_VALUE_OP: + { + JsonbValue *jbv = JsonPathValue(item, path, empty, error, + pre_eval->args); + + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + if (!jbv) /* NULL or empty */ + { + resnull = true; + break; + } + + Assert(!*empty); + + resnull = false; + + /* coerce scalar item to the output type */ + if (jexpr->returning->typid == JSONOID || + jexpr->returning->typid == JSONBOID) + { + /* Use result coercion from json[b] to the output type */ + res = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + break; + } + + /* + * Error out if no cast exists to coerce SQL/JSON item to the + * the output type. + */ + Assert(post_eval->item_jcstate == NULL); + res = ExecPrepareJsonItemCoercion(jbv, + jsestate->item_jcstates, + &post_eval->item_jcstate); + if (post_eval->item_jcstate && + post_eval->item_jcstate->coercion && + (post_eval->item_jcstate->coercion->via_io || + post_eval->item_jcstate->coercion->via_populate)) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * Coercion via I/O means here that the cast to the target + * type simply does not exist. + */ + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE), + errmsg("SQL/JSON item cannot be cast to target type"))); + } + break; + } + + case JSON_EXISTS_OP: + { + bool exists = JsonPathExists(item, path, + pre_eval->args, + error); + + resnull = error && *error; + res = BoolGetDatum(exists); + break; + } + + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * If the ON EMPTY behavior is to cause an error, do so here. Other + * behaviors will be handled by the caller. + */ + if (*empty) + { + Assert(jexpr->on_empty); /* it is not JSON_EXISTS */ + + if (jexpr->on_empty->btype == JSON_BEHAVIOR_ERROR) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + ereport(ERROR, + (errcode(ERRCODE_NO_SQL_JSON_ITEM), + errmsg("no SQL/JSON item"))); + } + } + + *op->resvalue = res; + *op->resnull = resnull; +} + +/* + * Skip calling ExecEvalJson() on the given JsonExpr? + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_skip.jsestate; + + /* + * Skip if either of the input expressions has turned out to be NULL, + * though do execute domain checks for NULLs, which are handled by the + * coercion step. + */ + if (jsestate->pre_eval.formatted_expr.isnull || + jsestate->pre_eval.pathspec.isnull) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_skip.jump_coercion; + } + + /* + * Go evaluate the PASSING args if any and subsequently JSON path itself. + */ + return op->d.jsonexpr_skip.jump_passing_args; +} + +/* + * Returns the step address to perform the JsonBehavior applicable to + * the JSON item that resulted from evaluating the given JsonExpr. + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_behavior.jsestate; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonBehavior *behavior = NULL; + int jump_to = -1; + + if (post_eval->error || post_eval->coercion_error) + { + behavior = jsestate->jsexpr->on_error; + jump_to = op->d.jsonexpr_behavior.jump_onerror_expr; + } + else if (post_eval->empty) + { + behavior = jsestate->jsexpr->on_empty; + jump_to = op->d.jsonexpr_behavior.jump_onempty_expr; + } + else if (!post_eval->coercion_done) + { + /* + * If no error or the JSON item is not empty, directly go to the + * coercion step to coerce the item as is. + */ + return op->d.jsonexpr_behavior.jump_coercion; + } + + Assert(behavior); + + /* + * Set up for coercion step that will run to coerce a non-default behavior + * value. It should use result_coercion, if any. Errors that may occur + * should be thrown for JSON ops other than JSON_VALUE_OP. + */ + if (behavior->btype != JSON_BEHAVIOR_DEFAULT) + { + post_eval->item_jcstate = NULL; + jsestate->throw_error = (jsestate->jsexpr->op != JSON_VALUE_OP); + } + + Assert(jump_to >= 0); + return jump_to; +} + +/* + * Evaluate or return the step address to evaluate a coercion of a JSON item + * to the target type. The former if the coercion must be done right away by + * calling its input function or by calling json_populate_type(). + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull) +{ + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonExpr *jexpr = jsestate->jsexpr; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonCoercionState *item_jcstate = post_eval->item_jcstate; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + + if (item_jcstate == NULL && + jsestate->jsexpr->op != JSON_EXISTS_OP) + { + ErrorSaveContext escontext = {T_ErrorSaveContext}; + Node *escontext_p; + JsonCoercion *coercion; + Jsonb *jb; + + escontext_p = !jsestate->throw_error ? (Node *) &escontext : NULL; + coercion = result_jcstate ? result_jcstate->coercion : NULL; + jb = resnull ? NULL : DatumGetJsonbP(res); + + if ((coercion && coercion->via_io) || + (jexpr->omit_quotes && !resnull && + JB_ROOT_IS_SCALAR(jb))) + { + /* strip quotes and call typinput function */ + char *str = resnull ? NULL : JsonbUnquote(jb); + bool type_is_domain; + + type_is_domain = (getBaseType(jexpr->returning->typid) != + jexpr->returning->typid); + + /* + * Catch errors only if the type is not a domain, because errors + * caused by a domain's constraint failure must be thrown right + * away. + */ + if (!InputFunctionCallSafe(jsestate->input.finfo, str, + jsestate->input.typioparam, + jexpr->returning->typmod, + !type_is_domain ? escontext_p : NULL, + op->resvalue)) + { + post_eval->error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + else if (coercion && coercion->via_populate) + { + *op->resvalue = json_populate_type(res, JSONBOID, + jexpr->returning->typid, + jexpr->returning->typmod, + &post_eval->cache, + econtext->ecxt_per_query_memory, + op->resnull, + escontext_p); + if (SOFT_ERROR_OCCURRED(escontext_p)) + { + post_eval->error = true; + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + } + + if (!jsestate->throw_error) + { + post_eval->escontext.type = T_ErrorSaveContext; + state->escontext = (Node *) &post_eval->escontext; + } + + if (item_jcstate && item_jcstate->jump_eval_expr >= 0) + return item_jcstate->jump_eval_expr; + else if (result_jcstate && result_jcstate->jump_eval_expr >= 0) + return result_jcstate->jump_eval_expr; + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; +} + +/* + * Checks if the coercion evaluation led to an error. If an error did occur, + * this returns the address of the step that handles the error, otherwise + * the step after the coercion step, which finishes the JsonExpr processing. + */ +int +ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op) +{ + JsonExprPostEvalState *post_eval = + &op->d.jsonexpr_coercion_finish.jsestate->post_eval; + + if (SOFT_ERROR_OCCURRED(state->escontext)) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + post_eval->coercion_error = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_error; + } + + /* Reset. */ + state->escontext = NULL; + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_done; +} + +/* + * Prepare SQL/JSON item coercion to the output type. Returned a datum of the + * corresponding SQL type and a pointer to the coercion state. + */ +static Datum +ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate) +{ + JsonCoercionState *item_jcstate; + Datum res; + JsonbValue buf; + + if (item->type == jbvBinary && + JsonContainerIsScalar(item->val.binary.data)) + { + bool res PG_USED_FOR_ASSERTS_ONLY; + + res = JsonbExtractScalar(item->val.binary.data, &buf); + item = &buf; + Assert(res); + } + + /* get coercion state reference and datum of the corresponding SQL type */ + switch (item->type) + { + case jbvNull: + item_jcstate = item_jcstates->null; + res = (Datum) 0; + break; + + case jbvString: + item_jcstate = item_jcstates->string; + res = PointerGetDatum(cstring_to_text_with_len(item->val.string.val, + item->val.string.len)); + break; + + case jbvNumeric: + item_jcstate = item_jcstates->numeric; + res = NumericGetDatum(item->val.numeric); + break; + + case jbvBool: + item_jcstate = item_jcstates->boolean; + res = BoolGetDatum(item->val.boolean); + break; + + case jbvDatetime: + res = item->val.datetime.value; + switch (item->val.datetime.typid) + { + case DATEOID: + item_jcstate = item_jcstates->date; + break; + case TIMEOID: + item_jcstate = item_jcstates->time; + break; + case TIMETZOID: + item_jcstate = item_jcstates->timetz; + break; + case TIMESTAMPOID: + item_jcstate = item_jcstates->timestamp; + break; + case TIMESTAMPTZOID: + item_jcstate = item_jcstates->timestamptz; + break; + default: + elog(ERROR, "unexpected jsonb datetime type oid %u", + item->val.datetime.typid); + return (Datum) 0; + } + break; + + case jbvArray: + case jbvObject: + case jbvBinary: + item_jcstate = item_jcstates->composite; + res = JsonbPGetDatum(JsonbValueToJsonb(item)); + break; + + default: + elog(ERROR, "unexpected jsonb value type %d", item->type); + return (Datum) 0; + } + + *p_item_jcstate = item_jcstate; + + return res; +} + /* * ExecEvalGroupingFunc diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 00d7b8110b..da3870cba6 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -1860,6 +1860,256 @@ llvm_compile_expr(ExprState *state) LLVMBuildBr(b, opblocks[opno + 1]); break; + case EEOP_JSONEXPR_PATH: + build_EvalXFunc(b, mod, "ExecEvalJsonExpr", + v_state, op, v_econtext); + LLVMBuildBr(b, opblocks[opno + 1]); + break; + + case EEOP_JSONEXPR_SKIP: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprSkip() to decide if JSON path + * evaluation can be skipped. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprSkip"), + params, lengthof(params), ""); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_skip.jump_coercion, which signifies + * skipping of JSON path evaluation, else to the next step + * which must point to the steps to evaluate PASSING args, + * if any, or to the JSON path evaluation. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_skip.jump_coercion), + ""), + opblocks[op->d.jsonexpr_skip.jump_coercion], + opblocks[opno + 1]); + break; + } + + case EEOP_JSONEXPR_BEHAVIOR: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + LLVMBasicBlockRef b_jump_onerror_default; + + /* + * Call ExecEvalJsonExprBehavior() to decide if ON EMPTY + * or ON ERROR behavior must be invoked depending on what + * JSON path evaluation returned. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprBehavior"), + params, lengthof(params), ""); + + b_jump_onerror_default = + l_bb_before_v(opblocks[opno + 1], + "op.%d.jsonexpr_behavior_jump_onerror_default", opno); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_behavior.jump_coercion, else to the + * next block, one that checks whether to evaluate the ON + * ERROR default expression. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_coercion), + ""), + opblocks[op->d.jsonexpr_behavior.jump_coercion], + b_jump_onerror_default); + + /* + * Block that checks whether to evaluate the ON ERROR + * default expression. + * + * Jump to evaluate the ON ERROR default expression if the + * returned address is the same as + * jsonexpr_behavior.jump_onerror_default, else jump to + * evaluate the ON EMPTY default expression. + */ + LLVMPositionBuilderAtEnd(b, b_jump_onerror_default); + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_onerror_expr), + ""), + opblocks[op->d.jsonexpr_behavior.jump_onerror_expr], + opblocks[op->d.jsonexpr_behavior.jump_onempty_expr]); + break; + } + case EEOP_JSONEXPR_COERCION: + { + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonItemCoercionsState *item_jcstates = jsestate->item_jcstates; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + LLVMValueRef v_ret; + LLVMValueRef params[5]; + + /* + * Call ExecEvalJsonExprCoercion() to evaluate appropriate + * coercion. This will return the step address to jump + * to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + params[2] = v_econtext; + params[3] = v_resvaluep; + params[4] = v_resnullp; + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercion"), + params, lengthof(params), ""); + + /* + * Jump to handle a coercion error if the returned address + * is the same as jsonexpr_coercion.jump_coercion_error, + * else to the step after coercion (coercion done!). + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + + if (item_jcstates) + { + JsonCoercionState **item_jcstate; + int n_coercions = (int) + (item_jcstates->composite - item_jcstates->null) + 1; + int i; + LLVMBasicBlockRef *b_coercions; + + + /* + * Will create a block for each coercion below to + * check whether to evaluate the coercion's expression + * if there's one or to skip to the end if not. + */ + b_coercions = palloc((n_coercions + 1) * sizeof(LLVMBasicBlockRef)); + for (i = 0; i < n_coercions + 1; i++) + b_coercions[i] = + l_bb_before_v(opblocks[opno + 1], + "op.%d.json_item_coercion.%d", + opno, i); + + /* Jump to check first coercion */ + LLVMBuildBr(b, b_coercions[0]); + + /* + * Add conditional branches for individual coercion's + * expressions + */ + for (item_jcstate = &item_jcstates->null, i = 0; + item_jcstate <= &item_jcstates->composite; + item_jcstate++, i++) + { + /* Block for this coercion */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + + /* + * Jump to evaluate the coercion's expression if + * the address returned is the same as this + * coercion's jump_eval_expr (that is, if it is + * valid), else check the next coercion's. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const((*item_jcstate)->jump_eval_expr), + ""), + (*item_jcstate)->jump_eval_expr >= 0 ? + opblocks[(*item_jcstate)->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + b_coercions[i + 1]); + } + + /* + * A placeholder block that the last coercion's block + * might jump to, which unconditionally jumps to end + * of coercions. + */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + LLVMBuildBr(b, opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + + /* + * Jump to evaluate the result_coercion's expression if + * none of the above coercions matched, that is, if + * there's one. + */ + if (result_jcstate) + { + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(result_jcstate->jump_eval_expr), + ""), + result_jcstate->jump_eval_expr >= 0 ? + opblocks[result_jcstate->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + break; + } + + case EEOP_JSONEXPR_COERCION_FINISH: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprCoercionFinish() to check whether + * an coercion error occurred, in which case we must jump + * to whatever step handles the error. This returns the + * step address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercionFinish"), + params, lengthof(params), ""); + + /* + * Jump to the step that handles coercion error if the + * returned address is the same as + * jsonexpr_coercion_finish.jump_coercion_error, else to + * jsonexpr_coercion_finish.jump_coercion_done. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion_finish.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_done]); + break; + } + case EEOP_AGGREF: { LLVMValueRef v_aggno; diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c index 41ac4c6f45..cf3ced3427 100644 --- a/src/backend/jit/llvm/llvmjit_types.c +++ b/src/backend/jit/llvm/llvmjit_types.c @@ -135,6 +135,11 @@ void *referenced_functions[] = ExecEvalXmlExpr, ExecEvalJsonConstructor, ExecEvalJsonIsPredicate, + ExecEvalJsonExprSkip, + ExecEvalJsonExprBehavior, + ExecEvalJsonExprCoercion, + ExecEvalJsonExprCoercionFinish, + ExecEvalJsonExpr, MakeExpandedObjectReadOnlyInternal, slot_getmissingattrs, slot_getsomeattrs_int, diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 39e1884cf4..5dc3aa2e9f 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -859,6 +859,21 @@ makeJsonValueExpr(Expr *expr, JsonFormat *format) return jve; } +/* + * makeJsonBehavior - + * creates a JsonBehavior node + */ +JsonBehavior * +makeJsonBehavior(JsonBehaviorType type, Node *default_expr) +{ + JsonBehavior *behavior = makeNode(JsonBehavior); + + behavior->btype = type; + behavior->default_expr = default_expr; + + return behavior; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index dda964bd19..d31371bb4d 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -236,6 +236,12 @@ exprType(const Node *expr) case T_JsonIsPredicate: type = BOOLOID; break; + case T_JsonExpr: + type = ((const JsonExpr *) expr)->returning->typid; + break; + case T_JsonCoercion: + type = exprType(((const JsonCoercion *) expr)->expr); + break; case T_NullTest: type = BOOLOID; break; @@ -495,6 +501,10 @@ exprTypmod(const Node *expr) return exprTypmod((Node *) ((const JsonValueExpr *) expr)->formatted_expr); case T_JsonConstructorExpr: return ((const JsonConstructorExpr *) expr)->returning->typmod; + case T_JsonExpr: + return ((JsonExpr *) expr)->returning->typmod; + case T_JsonCoercion: + return exprTypmod(((const JsonCoercion *) expr)->expr); case T_CoerceToDomain: return ((const CoerceToDomain *) expr)->resulttypmod; case T_CoerceToDomainValue: @@ -971,6 +981,22 @@ exprCollation(const Node *expr) /* IS JSON's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + coll = InvalidOid; + else if (coercion->expr) + coll = exprCollation(coercion->expr); + else if (coercion->via_io || coercion->via_populate) + coll = coercion->collation; + else + coll = InvalidOid; + } + break; + case T_NullTest: /* NullTest's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ @@ -1207,6 +1233,21 @@ exprSetCollation(Node *expr, Oid collation) case T_JsonIsPredicate: Assert(!OidIsValid(collation)); /* result is always boolean */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + Assert(!OidIsValid(collation)); + else if (coercion->expr) + exprSetCollation(coercion->expr, collation); + else if (coercion->via_io || coercion->via_populate) + coercion->collation = collation; + else + Assert(!OidIsValid(collation)); + } + break; case T_NullTest: /* NullTest's result is boolean ... */ Assert(!OidIsValid(collation)); /* ... so never set a collation */ @@ -1510,6 +1551,15 @@ exprLocation(const Node *expr) case T_JsonIsPredicate: loc = ((const JsonIsPredicate *) expr)->location; break; + case T_JsonExpr: + { + const JsonExpr *jsexpr = (const JsonExpr *) expr; + + /* consider both function name and leftmost arg */ + loc = leftmostLoc(jsexpr->location, + exprLocation(jsexpr->formatted_expr)); + } + break; case T_NullTest: { const NullTest *nexpr = (const NullTest *) expr; @@ -2262,6 +2312,54 @@ expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + if (WALK(jexpr->formatted_expr)) + return true; + if (WALK(jexpr->result_coercion)) + return true; + if (WALK(jexpr->passing_values)) + return true; + /* we assume walker doesn't care about passing_names */ + if (jexpr->on_empty && + WALK(jexpr->on_empty->default_expr)) + return true; + if (WALK(jexpr->on_error->default_expr)) + return true; + if (WALK(jexpr->coercions)) + return true; + } + break; + case T_JsonCoercion: + return WALK(((JsonCoercion *) node)->expr); + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + + if (WALK(coercions->null)) + return true; + if (WALK(coercions->string)) + return true; + if (WALK(coercions->numeric)) + return true; + if (WALK(coercions->boolean)) + return true; + if (WALK(coercions->date)) + return true; + if (WALK(coercions->time)) + return true; + if (WALK(coercions->timetz)) + return true; + if (WALK(coercions->timestamp)) + return true; + if (WALK(coercions->timestamptz)) + return true; + if (WALK(coercions->composite)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: @@ -3261,6 +3359,54 @@ expression_tree_mutator_impl(Node *node, return (Node *) newnode; } + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + JsonExpr *newnode; + + FLATCOPY(newnode, jexpr, JsonExpr); + MUTATE(newnode->path_spec, jexpr->path_spec, Node *); + MUTATE(newnode->formatted_expr, jexpr->formatted_expr, Node *); + MUTATE(newnode->result_coercion, jexpr->result_coercion, JsonCoercion *); + MUTATE(newnode->passing_values, jexpr->passing_values, List *); + /* assume mutator does not care about passing_names */ + if (newnode->on_empty) + MUTATE(newnode->on_empty->default_expr, + jexpr->on_empty->default_expr, Node *); + MUTATE(newnode->on_error->default_expr, + jexpr->on_error->default_expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonCoercion: + { + JsonCoercion *coercion = (JsonCoercion *) node; + JsonCoercion *newnode; + + FLATCOPY(newnode, coercion, JsonCoercion); + MUTATE(newnode->expr, coercion->expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + JsonItemCoercions *newnode; + + FLATCOPY(newnode, coercions, JsonItemCoercions); + MUTATE(newnode->null, coercions->null, JsonCoercion *); + MUTATE(newnode->string, coercions->string, JsonCoercion *); + MUTATE(newnode->numeric, coercions->numeric, JsonCoercion *); + MUTATE(newnode->boolean, coercions->boolean, JsonCoercion *); + MUTATE(newnode->date, coercions->date, JsonCoercion *); + MUTATE(newnode->time, coercions->time, JsonCoercion *); + MUTATE(newnode->timetz, coercions->timetz, JsonCoercion *); + MUTATE(newnode->timestamp, coercions->timestamp, JsonCoercion *); + MUTATE(newnode->timestamptz, coercions->timestamptz, JsonCoercion *); + MUTATE(newnode->composite, coercions->composite, JsonCoercion *); + return (Node *) newnode; + } + break; case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -3947,6 +4093,43 @@ raw_expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonArgument: + return WALK(((JsonArgument *) node)->val); + case T_JsonCommon: + { + JsonCommon *jc = (JsonCommon *) node; + + if (WALK(jc->expr)) + return true; + if (WALK(jc->pathspec)) + return true; + if (WALK(jc->passing)) + return true; + } + break; + case T_JsonBehavior: + { + JsonBehavior *jb = (JsonBehavior *) node; + + if (jb->btype == JSON_BEHAVIOR_DEFAULT && + WALK(jb->default_expr)) + return true; + } + break; + case T_JsonFuncExpr: + { + JsonFuncExpr *jfe = (JsonFuncExpr *) node; + + if (WALK(jfe->common)) + return true; + if (jfe->output && WALK(jfe->output)) + return true; + if (WALK(jfe->on_empty)) + return true; + if (WALK(jfe->on_error)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index ef475d95a1..f58c275b4b 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -4609,7 +4609,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context) IsA(node, SQLValueFunction) || IsA(node, XmlExpr) || IsA(node, CoerceToDomain) || - IsA(node, NextValueExpr)) + IsA(node, NextValueExpr) || + IsA(node, JsonExpr)) { /* Treat all these as having cost 1 */ context->total.per_tuple += cpu_operator_cost; diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 59d8ce789e..2dacb83052 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -53,6 +53,7 @@ #include "utils/fmgroids.h" #include "utils/json.h" #include "utils/jsonb.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/syscache.h" @@ -412,6 +413,24 @@ contain_mutable_functions_walker(Node *node, void *context) /* Check all subnodes */ } + if (IsA(node, JsonExpr)) + { + JsonExpr *jexpr = castNode(JsonExpr, node); + Const *cnst; + + if (!IsA(jexpr->path_spec, Const)) + return true; + + cnst = castNode(Const, jexpr->path_spec); + + Assert(cnst->consttype == JSONPATHOID); + if (cnst->constisnull) + return false; + + return jspIsMutable(DatumGetJsonPathP(cnst->constvalue), + jexpr->passing_names, jexpr->passing_values); + } + if (IsA(node, SQLValueFunction)) { /* all variants of SQLValueFunction are stable */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 341b002dc7..f7ad8f18de 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -278,6 +278,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MergeWhenClause *mergewhen; struct KeyActions *keyactions; struct KeyAction *keyaction; + JsonBehavior *jsbehavior; + JsonQuotes js_quotes; } %type <node> stmt toplevel_stmt schema_stmt routine_body_stmt @@ -650,14 +652,22 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_returning_clause_opt json_name_and_value json_aggregate_func + json_api_common_syntax + json_argument %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt + json_arguments %type <ival> json_encoding_clause_opt json_predicate_type_constraint + json_wrapper_behavior %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt +%type <jsbehavior> json_value_behavior + json_query_behavior + json_exists_behavior +%type <js_quotes> json_quotes_clause_opt /* * Non-keyword token types. These are hard-wired into the "flex" lexer. @@ -694,7 +704,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT - COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT + COMMITTED COMPRESSION CONCURRENTLY CONDITIONAL CONFIGURATION CONFLICT CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY COST CREATE CROSS CSV CUBE CURRENT_P CURRENT_CATALOG CURRENT_DATE CURRENT_ROLE CURRENT_SCHEMA @@ -705,8 +715,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P DOUBLE_P DROP - EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT - EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION + EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE + EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION EXTENSION EXTERNAL EXTRACT FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR @@ -721,10 +731,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION - JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG - JSON_SCALAR JSON_SERIALIZE + JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE - KEY KEYS + KEY KEYS KEEP LABEL LANGUAGE LARGE_P LAST_P LATERAL_P LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL @@ -738,7 +748,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC - OBJECT_P OF OFF OFFSET OIDS OLD ON ONLY OPERATOR OPTION OPTIONS OR + OBJECT_P OF OFF OFFSET OIDS OLD OMIT ON ONLY OPERATOR OPTION OPTIONS OR ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER @@ -747,7 +757,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION - QUOTE + QUOTE QUOTES RANGE READ REAL REASSIGN RECHECK RECURSIVE REF_P REFERENCES REFERENCING REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA @@ -758,7 +768,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); SEQUENCE SEQUENCES SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P - START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P + START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRING_P STRIP_P SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN @@ -766,7 +776,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); TREAT TRIGGER TRIM TRUE_P TRUNCATE TRUSTED TYPE_P TYPES_P - UESCAPE UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN + UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING @@ -15663,6 +15673,192 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_error = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->on_error = $10; + n->location = @1; + $$ = (Node *) n; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->location = @1; + $$ = (Node *) p; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + json_exists_behavior ON ERROR_P + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->on_error = $5; + p->location = @1; + $$ = (Node *) p; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->on_error = $8; + n->location = @1; + $$ = (Node *) n; + } ; @@ -16389,6 +16585,72 @@ opt_asymmetric: ASYMMETRIC ; /* SQL/JSON support */ +json_api_common_syntax: + json_value_expr ',' a_expr /* i.e. a json_path */ + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + + | json_value_expr ',' a_expr /* i.e. a json_path */ + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = $5; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = $7; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_arguments: + json_argument { $$ = list_make1($1); } + | json_arguments ',' json_argument { $$ = lappend($1, $3); } + ; + +json_argument: + json_value_expr AS ColLabel + { + JsonArgument *n = makeNode(JsonArgument); + + n->val = (JsonValueExpr *) $1; + n->name = $3; + $$ = (Node *) n; + } + ; + json_value_expr: a_expr json_format_clause_opt { @@ -16412,6 +16674,50 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; +json_query_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + | EMPTY_P ARRAY { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + | EMPTY_P OBJECT_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + /* non-standard, for Oracle compatibility only */ + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + ; + +json_exists_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | TRUE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_TRUE, NULL); } + | FALSE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_FALSE, NULL); } + | UNKNOWN { $$ = makeJsonBehavior(JSON_BEHAVIOR_UNKNOWN, NULL); } + ; + +json_value_behavior: + NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + ; + +/* ARRAY is a noise word */ +json_wrapper_behavior: + WITHOUT WRAPPER { $$ = JSW_NONE; } + | WITHOUT ARRAY WRAPPER { $$ = JSW_NONE; } + | WITH WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL ARRAY WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL WRAPPER { $$ = JSW_UNCONDITIONAL; } + | /* empty */ { $$ = JSW_NONE; } + ; + +json_quotes_clause_opt: + KEEP QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_KEEP; } + | KEEP QUOTES { $$ = JS_QUOTES_KEEP; } + | OMIT QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_OMIT; } + | OMIT QUOTES { $$ = JS_QUOTES_OMIT; } + | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } + ; + json_returning_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17014,6 +17320,7 @@ unreserved_keyword: | COMMIT | COMMITTED | COMPRESSION + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17050,10 +17357,12 @@ unreserved_keyword: | DOUBLE_P | DROP | EACH + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17103,6 +17412,7 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION + | KEEP | KEY | KEYS | LABEL @@ -17149,6 +17459,7 @@ unreserved_keyword: | OFF | OIDS | OLD + | OMIT | OPERATOR | OPTION | OPTIONS @@ -17179,6 +17490,7 @@ unreserved_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REASSIGN @@ -17238,6 +17550,7 @@ unreserved_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUPPORT @@ -17260,6 +17573,7 @@ unreserved_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNKNOWN | UNLISTEN @@ -17320,10 +17634,13 @@ col_name_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE | LEAST | NATIONAL | NCHAR @@ -17556,6 +17873,7 @@ bare_label_keyword: | COMMITTED | COMPRESSION | CONCURRENTLY + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17608,11 +17926,13 @@ bare_label_keyword: | DROP | EACH | ELSE + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | END_P | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17682,10 +18002,14 @@ bare_label_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE + | KEEP | KEY | KEYS | LABEL @@ -17746,6 +18070,7 @@ bare_label_keyword: | OFF | OIDS | OLD + | OMIT | ONLY | OPERATOR | OPTION @@ -17783,6 +18108,7 @@ bare_label_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REAL @@ -17851,6 +18177,7 @@ bare_label_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUBSTRING @@ -17885,6 +18212,7 @@ bare_label_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNIQUE | UNKNOWN diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c index 9f6afc351c..692e5d1225 100644 --- a/src/backend/parser/parse_collate.c +++ b/src/backend/parser/parse_collate.c @@ -691,6 +691,13 @@ assign_collations_walker(Node *node, assign_collations_context *context) &loccontext); } break; + case T_JsonExpr: + + /* + * Context item and PASSING arguments are already + * marked with collations in parse_expr.c. + */ + break; default: /* diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index e978dc3e76..2791ce85f4 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -90,6 +90,8 @@ static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); static Node *transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr); +static Node *transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve); +static Node *transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *p); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -353,6 +355,14 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); break; + case T_JsonFuncExpr: + result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); + break; + + case T_JsonValueExpr: + result = transformJsonValueExpr(pstate, (JsonValueExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3238,10 +3248,10 @@ makeCaseTestExpr(Node *expr) * default format otherwise. */ static Node * -transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, - char *constructName, - JsonFormatType default_format, - Oid targettype) +transformJsonValueExprInternal(ParseState *pstate, JsonValueExpr *ve, + char *constructName, + JsonFormatType default_format, + Oid targettype, bool isarg) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3278,6 +3288,35 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, else format = ve->format->format_type; } + else if (isarg) + { + /* Pass SQL/JSON item types directly without conversion to json[b]. */ + switch (exprtype) + { + case TEXTOID: + case NUMERICOID: + case BOOLOID: + case INT2OID: + case INT4OID: + case INT8OID: + case FLOAT4OID: + case FLOAT8OID: + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + return expr; + + default: + if (typcategory == TYPCATEGORY_STRING) + return expr; + /* else convert argument to json[b] type */ + break; + } + + format = default_format; + } else if (exprtype == JSONOID || exprtype == JSONBOID) format = JS_FORMAT_DEFAULT; /* do not format json[b] types */ else @@ -3289,7 +3328,8 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, Node *coerced; bool cast_is_needed = OidIsValid(targettype); - if (!cast_is_needed && + if (!isarg && + !cast_is_needed && exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), @@ -3354,6 +3394,20 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, return expr; } +/* Wrapper with an interface for use in transformExpr() */ +static Node * +transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve) +{ + /* + * A bit bogus to use "JSON()" here for the parent construct's name but + * there's no good way to know the enclosing JSON expression when called + * through transformExpr(). + */ + return transformJsonValueExprInternal(pstate, ve, "JSON()", + JS_FORMAT_DEFAULT, + InvalidOid, false); +} + /* * Checks specified output format for its applicability to the target type. */ @@ -3613,10 +3667,12 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) { JsonKeyValue *kv = castNode(JsonKeyValue, lfirst(lc)); Node *key = transformExprRecurse(pstate, (Node *) kv->key); - Node *val = transformJsonValueExpr(pstate, kv->value, - "JSON_OBJECT()", - JS_FORMAT_DEFAULT, - InvalidOid); + Node *val = transformJsonValueExprInternal(pstate, + kv->value, + "JSON_OBJECT()", + JS_FORMAT_DEFAULT, + InvalidOid, + false); args = lappend(args, key); args = lappend(args, val); @@ -3795,8 +3851,10 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) Oid aggtype; key = transformExprRecurse(pstate, (Node *) agg->arg->key); - val = transformJsonValueExpr(pstate, agg->arg->value, "JSON_OBJECTAGG()", - JS_FORMAT_DEFAULT, InvalidOid); + val = transformJsonValueExprInternal(pstate, agg->arg->value, + "JSON_OBJECTAGG()", + JS_FORMAT_DEFAULT, + InvalidOid, false); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3852,8 +3910,8 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) Oid aggfnoid; Oid aggtype; - arg = transformJsonValueExpr(pstate, agg->arg, "JSON_ARRAYAGG()", - JS_FORMAT_DEFAULT, InvalidOid); + arg = transformJsonValueExprInternal(pstate, agg->arg, "JSON_ARRAYAGG()", + JS_FORMAT_DEFAULT, InvalidOid, false); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3899,10 +3957,10 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) foreach(lc, ctor->exprs) { JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); - Node *val = transformJsonValueExpr(pstate, jsval, - "JSON_ARRAY()", - JS_FORMAT_DEFAULT, - InvalidOid); + Node *val = transformJsonValueExprInternal(pstate, jsval, + "JSON_ARRAY()", + JS_FORMAT_DEFAULT, + InvalidOid, false); args = lappend(args, val); } @@ -4072,8 +4130,9 @@ transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) * Coerce argument to target type using CAST for compatibility with PG * function-like CASTs. */ - arg = transformJsonValueExpr(pstate, jsexpr->expr, "JSON()", - JS_FORMAT_JSON, returning->typid); + arg = transformJsonValueExprInternal(pstate, jsexpr->expr, "JSON()", + JS_FORMAT_JSON, returning->typid, + false); } return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, @@ -4119,10 +4178,10 @@ transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *jsexpr) static Node * transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) { - Node *arg = transformJsonValueExpr(pstate, expr->expr, - "JSON_SERIALIZE()", - JS_FORMAT_JSON, - InvalidOid); + Node *arg = transformJsonValueExprInternal(pstate, expr->expr, + "JSON_SERIALIZE()", + JS_FORMAT_JSON, + InvalidOid, false); JsonReturning *returning; if (expr->output) @@ -4157,3 +4216,457 @@ transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), NULL, returning, false, false, expr->location); } + +/* + * Transform a JSON PASSING clause. + */ +static void +transformJsonPassingArgs(ParseState *pstate, JsonFormatType format, List *args, + List **passing_values, List **passing_names) +{ + ListCell *lc; + + *passing_values = NIL; + *passing_names = NIL; + + foreach(lc, args) + { + JsonArgument *arg = castNode(JsonArgument, lfirst(lc)); + Node *expr = transformJsonValueExprInternal(pstate, arg->val, + "JSON PASSING()", + format, InvalidOid, + true); + + assign_expr_collations(pstate, expr); + + *passing_values = lappend(*passing_values, expr); + *passing_names = lappend(*passing_names, makeString(arg->name)); + } +} + +/* + * Transform a JSON BEHAVIOR clause. + */ +static JsonBehavior * +transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior, + JsonBehaviorType default_behavior) +{ + JsonBehaviorType behavior_type = default_behavior; + Node *default_expr = NULL; + + if (behavior) + { + behavior_type = behavior->btype; + if (behavior_type == JSON_BEHAVIOR_DEFAULT) + default_expr = transformExprRecurse(pstate, behavior->default_expr); + } + return makeJsonBehavior(behavior_type, default_expr); +} + +/* + * Common code for JSON_VALUE, JSON_QUERY, JSON_EXISTS transformation + * into a JsonExpr node. + */ +static JsonExpr * +transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = makeNode(JsonExpr); + Node *pathspec; + JsonFormatType format; + char *constructName; + + if (func->common->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("JSON_TABLE path name is not allowed here"), + parser_errposition(pstate, func->location))); + + jsexpr->location = func->location; + jsexpr->op = func->op; + switch (jsexpr->op) + { + case JSON_VALUE_OP: + constructName = "JSON_VALUE()"; + break; + case JSON_QUERY_OP: + constructName = "JSON_QUERY()"; + break; + case JSON_EXISTS_OP: + constructName = "JSON_EXISTS()"; + break; + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); + break; + } + jsexpr->formatted_expr = transformJsonValueExprInternal(pstate, + func->common->expr, + constructName, + JS_FORMAT_DEFAULT, + InvalidOid, false); + + assign_expr_collations(pstate, jsexpr->formatted_expr); + + /* format is determined by context item type */ + format = exprType(jsexpr->formatted_expr) == JSONBOID ? JS_FORMAT_JSONB : JS_FORMAT_JSON; + + jsexpr->result_coercion = NULL; + jsexpr->omit_quotes = false; + + jsexpr->format = func->common->expr->format; + + pathspec = transformExprRecurse(pstate, func->common->pathspec); + + jsexpr->path_spec = + coerce_to_target_type(pstate, pathspec, exprType(pathspec), + JSONPATHOID, -1, + COERCION_EXPLICIT, COERCE_IMPLICIT_CAST, + exprLocation(pathspec)); + if (!jsexpr->path_spec) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("JSON path expression must be type %s, not type %s", + "jsonpath", format_type_be(exprType(pathspec))), + parser_errposition(pstate, exprLocation(pathspec)))); + + /* transform and coerce to json[b] passing arguments */ + transformJsonPassingArgs(pstate, format, func->common->passing, + &jsexpr->passing_values, &jsexpr->passing_names); + + if (func->op != JSON_EXISTS_OP) + jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, + JSON_BEHAVIOR_NULL); + + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + func->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : + JSON_BEHAVIOR_NULL); + + return jsexpr; +} + +/* + * Assign default JSON returning type from the specified format or from + * the context item type. + */ +static void +assignDefaultJsonReturningType(Node *context_item, JsonFormat *context_format, + JsonReturning *ret) +{ + bool is_jsonb; + + ret->format = copyObject(context_format); + + if (ret->format->format_type == JS_FORMAT_DEFAULT) + is_jsonb = exprType(context_item) == JSONBOID; + else + is_jsonb = ret->format->format_type == JS_FORMAT_JSONB; + + ret->typid = is_jsonb ? JSONBOID : JSONOID; + ret->typmod = -1; +} + +/* + * Try to coerce expression to the output type or + * use json_populate_type() for composite, array and domain types or + * use coercion via I/O. + */ +static JsonCoercion * +coerceJsonExpr(ParseState *pstate, Node *expr, const JsonReturning *returning) +{ + char typtype; + JsonCoercion *coercion = makeNode(JsonCoercion); + + coercion->expr = coerceJsonFuncExpr(pstate, expr, returning, false); + + if (coercion->expr) + { + if (coercion->expr == expr) + coercion->expr = NULL; + + return coercion; + } + + typtype = get_typtype(returning->typid); + + if (returning->typid == RECORDOID || + typtype == TYPTYPE_COMPOSITE || + typtype == TYPTYPE_DOMAIN || + type_is_array(returning->typid)) + coercion->via_populate = true; + else + coercion->via_io = true; + + return coercion; +} + +/* + * Transform a JSON output clause of JSON_VALUE and JSON_QUERY. + */ +static void +transformJsonFuncExprOutput(ParseState *pstate, JsonFuncExpr *func, + JsonExpr *jsexpr) +{ + Node *expr = jsexpr->formatted_expr; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + /* JSON_VALUE returns text by default */ + if (func->op == JSON_VALUE_OP && !OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = TEXTOID; + jsexpr->returning->typmod = -1; + } + + if (OidIsValid(jsexpr->returning->typid)) + { + JsonReturning ret; + + if (func->op == JSON_VALUE_OP && + jsexpr->returning->typid != JSONOID && + jsexpr->returning->typid != JSONBOID) + { + /* Forced coercion via I/O for JSON_VALUE for non-JSON types */ + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = NULL; + jsexpr->result_coercion->via_io = true; + return; + } + + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, &ret); + + if (ret.typid != jsexpr->returning->typid || + ret.typmod != jsexpr->returning->typmod) + { + Node *placeholder = makeCaseTestExpr(expr); + + Assert(((CaseTestExpr *) placeholder)->typeId == ret.typid); + Assert(((CaseTestExpr *) placeholder)->typeMod == ret.typmod); + + jsexpr->result_coercion = coerceJsonExpr(pstate, placeholder, + jsexpr->returning); + } + } + else + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, + jsexpr->returning); +} + +/* + * Coerce an expression in JSON DEFAULT behavior to the target output type. + */ +static Node * +coerceDefaultJsonExpr(ParseState *pstate, JsonExpr *jsexpr, Node *defexpr) +{ + int location; + Oid exprtype; + + if (!defexpr) + return NULL; + + exprtype = exprType(defexpr); + location = exprLocation(defexpr); + + if (location < 0) + location = jsexpr->location; + + defexpr = coerce_to_target_type(pstate, + defexpr, + exprtype, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!defexpr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast DEFAULT expression type %s to %s", + format_type_be(exprtype), + format_type_be(jsexpr->returning->typid)), + parser_errposition(pstate, location))); + + return defexpr; +} + +/* + * Initialize SQL/JSON item coercion from the SQL type "typid" to the target + * "returning" type. + */ +static JsonCoercion * +initJsonItemCoercion(ParseState *pstate, Oid typid, + const JsonReturning *returning) +{ + Node *expr; + + if (typid == UNKNOWNOID) + { + expr = (Node *) makeNullConst(UNKNOWNOID, -1, InvalidOid); + } + else + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + + placeholder->typeId = typid; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + expr = (Node *) placeholder; + } + + return coerceJsonExpr(pstate, expr, returning); +} + +static void +initJsonItemCoercions(ParseState *pstate, JsonItemCoercions *coercions, + const JsonReturning *returning, Oid contextItemTypeId) +{ + struct + { + JsonCoercion **coercion; + Oid typid; + } *p, + coercionTypids[] = + { + {&coercions->null, UNKNOWNOID}, + {&coercions->string, TEXTOID}, + {&coercions->numeric, NUMERICOID}, + {&coercions->boolean, BOOLOID}, + {&coercions->date, DATEOID}, + {&coercions->time, TIMEOID}, + {&coercions->timetz, TIMETZOID}, + {&coercions->timestamp, TIMESTAMPOID}, + {&coercions->timestamptz, TIMESTAMPTZOID}, + {&coercions->composite, contextItemTypeId}, + {NULL, InvalidOid} + }; + + for (p = coercionTypids; p->coercion; p++) + *p->coercion = initJsonItemCoercion(pstate, p->typid, returning); +} + +/* + * Transform JSON_VALUE, JSON_QUERY, JSON_EXISTS functions into a JsonExpr node. + */ +static Node * +transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = transformJsonExprCommon(pstate, func); + const char *func_name = NULL; + Node *contextItemExpr = jsexpr->formatted_expr; + + /* + * Disallow FORMAT specification in the RETURNING clause of JSON_EXISTS() + * and JSON_VALUE(). + */ + if (func->output && + (func->op == JSON_VALUE_OP || func->op == JSON_EXISTS_OP)) + { + JsonFormat *format = func->output->returning->format; + + if(format->format_type != JS_FORMAT_DEFAULT || + format->encoding != JS_ENC_DEFAULT) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot specify FORMAT in RETURNING clause of %s", + func->op == JSON_VALUE_OP ? "JSON_VALUE()" : + "JSON_EXISTS()"), + parser_errposition(pstate, format->location))); + } + + switch (func->op) + { + case JSON_VALUE_OP: + func_name = "JSON_VALUE"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->coercions = makeNode(JsonItemCoercions); + initJsonItemCoercions(pstate, jsexpr->coercions, jsexpr->returning, + exprType(contextItemExpr)); + + break; + + case JSON_QUERY_OP: + func_name = "JSON_QUERY"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->wrapper = func->wrapper; + jsexpr->omit_quotes = func->omit_quotes; + + break; + + case JSON_EXISTS_OP: + func_name = "JSON_EXISTS"; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + if (!OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = BOOLOID; + jsexpr->returning->typmod = -1; + } + else if (jsexpr->returning->typid != BOOLOID) + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + int location = exprLocation((Node *) jsexpr); + + placeholder->typeId = BOOLOID; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = + coerce_to_target_type(pstate, (Node *) placeholder, BOOLOID, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!jsexpr->result_coercion->expr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(BOOLOID), + format_type_be(jsexpr->returning->typid)), + parser_coercion_errposition(pstate, location, (Node *) jsexpr))); + + if (jsexpr->result_coercion->expr == (Node *) placeholder) + jsexpr->result_coercion->expr = NULL; + } + break; + } + + if (exprType(contextItemExpr) != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("%s() is not yet implemented for the json type", func_name), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + return (Node *) jsexpr; +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 520d4f2a23..4f94fc69d6 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1979,6 +1979,21 @@ FigureColnameInternal(Node *node, char **name) /* make JSON_ARRAYAGG act like a regular function */ *name = "json_arrayagg"; return 2; + case T_JsonFuncExpr: + /* make SQL/JSON functions act like a regular function */ + switch (((JsonFuncExpr *) node)->op) + { + case JSON_QUERY_OP: + *name = "json_query"; + return 2; + case JSON_VALUE_OP: + *name = "json_value"; + return 2; + case JSON_EXISTS_OP: + *name = "json_exists"; + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index e27ea8ef97..188b5594e0 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -4426,6 +4426,50 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, } } +/* + * Parses the datetime format string in 'fmt_str' and returns true if it + * contains a timezone specifier, false if not. + */ +bool +datetime_format_has_tz(const char *fmt_str) +{ + bool incache; + int fmt_len = strlen(fmt_str); + int result; + FormatNode *format; + + if (fmt_len > DCH_CACHE_SIZE) + { + /* + * Allocate new memory if format picture is bigger than static cache + * and do not use cache (call parser always) + */ + incache = false; + + format = (FormatNode *) palloc((fmt_len + 1) * sizeof(FormatNode)); + + parse_format(format, fmt_str, DCH_keywords, + DCH_suff, DCH_index, DCH_FLAG, NULL); + } + else + { + /* + * Use cache buffers + */ + DCHCacheEntry *ent = DCH_cache_fetch(fmt_str, false); + + incache = true; + format = ent->format; + } + + result = DCH_datetime_type(format); + + if (!incache) + pfree(format); + + return result & DCH_ZONED; +} + /* * do_to_timestamp: shared code for to_timestamp and to_date * diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index 7a91177a55..100dfa9c3a 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -2265,3 +2265,65 @@ jsonb_float8(PG_FUNCTION_ARGS) PG_RETURN_DATUM(retValue); } + +/* + * Construct an empty array jsonb. + */ +Jsonb * +JsonbMakeEmptyArray(void) +{ + JsonbValue jbv; + + jbv.type = jbvArray; + jbv.val.array.elems = NULL; + jbv.val.array.nElems = 0; + jbv.val.array.rawScalar = false; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Construct an empty object jsonb. + */ +Jsonb * +JsonbMakeEmptyObject(void) +{ + JsonbValue jbv; + + jbv.type = jbvObject; + jbv.val.object.pairs = NULL; + jbv.val.object.nPairs = 0; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Convert jsonb to a C-string stripping quotes from scalar strings. + */ +char * +JsonbUnquote(Jsonb *jb) +{ + if (JB_ROOT_IS_SCALAR(jb)) + { + JsonbValue v; + + (void) JsonbExtractScalar(&jb->root, &v); + + if (v.type == jbvString) + return pnstrdup(v.val.string.val, v.val.string.len); + else if (v.type == jbvBool) + return pstrdup(v.val.boolean ? "true" : "false"); + else if (v.type == jbvNumeric) + return DatumGetCString(DirectFunctionCall1(numeric_out, + PointerGetDatum(v.val.numeric))); + else if (v.type == jbvNull) + return pstrdup("null"); + else + { + elog(ERROR, "unrecognized jsonb value type %d", v.type); + return NULL; + } + } + else + return JsonbToCString(NULL, &jb->root, VARSIZE(jb)); +} diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index 70cb922e6b..cbc6b94fc2 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -264,6 +264,7 @@ typedef struct PopulateArrayContext int *dims; /* dimensions */ int *sizes; /* current dimension counters */ int ndims; /* number of dimensions */ + Node *escontext; /* For soft-error capture */ } PopulateArrayContext; /* state for populate_array_json() */ @@ -441,12 +442,13 @@ static void JsValueToJsObject(JsValue *jsv, JsObject *jso); static Datum populate_composite(CompositeIOData *io, Oid typid, const char *colname, MemoryContext mcxt, HeapTupleHeader defaultval, JsValue *jsv, bool isnull); -static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv); +static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext); static void prepare_column_cache(ColumnIOData *column, Oid typid, int32 typmod, MemoryContext mcxt, bool need_scalar); static Datum populate_record_field(ColumnIOData *col, Oid typid, int32 typmod, const char *colname, MemoryContext mcxt, Datum defaultval, - JsValue *jsv, bool *isnull); + JsValue *jsv, bool *isnull, Node *escontext); static RecordIOData *allocate_record_info(MemoryContext mcxt, int ncolumns); static bool JsObjectGetField(JsObject *obj, char *field, JsValue *jsv); static void populate_recordset_record(PopulateRecordsetState *state, JsObject *obj); @@ -458,7 +460,8 @@ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims); static void populate_array_check_dimension(PopulateArrayContext *ctx, int ndim); static void populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv); static Datum populate_array(ArrayIOData *aio, const char *colname, - MemoryContext mcxt, JsValue *jsv); + MemoryContext mcxt, JsValue *jsv, Node *escontext, + bool *isnull); static Datum populate_domain(DomainIOData *io, Oid typid, const char *colname, MemoryContext mcxt, JsValue *jsv, bool isnull); @@ -2483,12 +2486,12 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) if (ndim <= 0) { if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the value of key \"%s\".", ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"))); } @@ -2505,13 +2508,13 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) appendStringInfo(&indices, "[%d]", ctx->sizes[i]); if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s of key \"%s\".", indices.data, ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s.", @@ -2519,7 +2522,11 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) } } -/* set the number of dimensions of the populated array when it becomes known */ +/* + * Set the number of dimensions of the populated array when it becomes known. + * + * Returns without doing anything if the input (ndims) is erratic. + */ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) { @@ -2530,6 +2537,10 @@ populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) if (ndims <= 0) populate_array_report_expected_array(ctx, ndims); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + ctx->ndims = ndims; ctx->dims = palloc(sizeof(int) * ndims); ctx->sizes = palloc0(sizeof(int) * ndims); @@ -2547,12 +2558,16 @@ populate_array_check_dimension(PopulateArrayContext *ctx, int ndim) if (ctx->dims[ndim] == -1) ctx->dims[ndim] = dim; /* assign dimension if not yet known */ else if (ctx->dims[ndim] != dim) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("malformed JSON array"), errdetail("Multidimensional arrays must have " "sub-arrays with matching dimensions."))); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + /* reset the current array dimension size counter */ ctx->sizes[ndim] = 0; @@ -2572,7 +2587,10 @@ populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv) ctx->aio->element_type, ctx->aio->element_typmod, NULL, ctx->mcxt, PointerGetDatum(NULL), - jsv, &element_isnull); + jsv, &element_isnull, ctx->escontext); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; accumArrayResult(ctx->astate, element, element_isnull, ctx->aio->element_type, ctx->acxt); @@ -2593,6 +2611,10 @@ populate_array_object_start(void *_state) else if (ndim < state->ctx->ndims) populate_array_report_expected_array(state->ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2610,6 +2632,10 @@ populate_array_array_end(void *_state) if (ndim < ctx->ndims) populate_array_check_dimension(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2685,6 +2711,10 @@ populate_array_scalar(void *_state, char *token, JsonTokenType tokentype) else if (ndim < ctx->ndims) populate_array_report_expected_array(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + if (ndim == ctx->ndims) { /* remember the scalar element token */ @@ -2714,7 +2744,13 @@ populate_array_json(PopulateArrayContext *ctx, char *json, int len) sem.array_element_end = populate_array_element_end; sem.scalar = populate_array_scalar; - pg_parse_json_or_ereport(state.lex, &sem); + if (!pg_parse_json_or_errsave(state.lex, &sem, ctx->escontext)) + { + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + pfree(state.lex); + return; + } /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2739,10 +2775,15 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ check_stack_depth(); - if (jbv->type != jbvBinary || !JsonContainerIsArray(jbc)) + if (jbv->type != jbvBinary || + !JsonContainerIsArray(jbc) || + JsonContainerIsScalar(jbc)) + { populate_array_report_expected_array(ctx, ndim - 1); - - Assert(!JsonContainerIsScalar(jbc)); + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + return; + } it = JsonbIteratorInit(jbc); @@ -2761,7 +2802,12 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ (tok == WJB_ELEM && (val.type != jbvBinary || !JsonContainerIsArray(val.val.binary.data))))) + { populate_array_assign_ndims(ctx, ndim); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + } jsv.is_json = false; jsv.val.jsonb = &val; @@ -2779,6 +2825,9 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ { /* populate child sub-array */ populate_array_dim_jsonb(ctx, &val, ndim + 1); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2796,12 +2845,18 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ Assert(tok == WJB_DONE && !it); } -/* recursively populate an array from json/jsonb */ +/* + * Recursively populate an array from json/jsonb + * + * *isnull is set to true if an error is reported during parsing. + */ static Datum populate_array(ArrayIOData *aio, const char *colname, MemoryContext mcxt, - JsValue *jsv) + JsValue *jsv, + Node *escontext, + bool *isnull) { PopulateArrayContext ctx; Datum result; @@ -2816,6 +2871,7 @@ populate_array(ArrayIOData *aio, ctx.ndims = 0; /* unknown yet */ ctx.dims = NULL; ctx.sizes = NULL; + ctx.escontext = escontext; if (jsv->is_json) populate_array_json(&ctx, jsv->val.json.str, @@ -2824,7 +2880,16 @@ populate_array(ArrayIOData *aio, else { populate_array_dim_jsonb(&ctx, jsv->val.jsonb, 1); - ctx.dims[0] = ctx.sizes[0]; + /* Nothing to do on an error. */ + if (!SOFT_ERROR_OCCURRED(ctx.escontext)) + ctx.dims[0] = ctx.sizes[0]; + } + + /* Nothing to return if an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx.escontext)) + { + *isnull = true; + return (Datum) 0; } Assert(ctx.ndims > 0); @@ -2841,6 +2906,7 @@ populate_array(ArrayIOData *aio, pfree(ctx.sizes); pfree(lbs); + *isnull = false; return result; } @@ -2956,7 +3022,8 @@ populate_composite(CompositeIOData *io, /* populate non-null scalar value from json/jsonb value */ static Datum -populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) +populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext) { Datum res; char *str = NULL; @@ -3027,7 +3094,11 @@ populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) elog(ERROR, "unrecognized jsonb type: %d", (int) jbv->type); } - res = InputFunctionCall(&io->typiofunc, str, io->typioparam, typmod); + if (!InputFunctionCallSafe(&io->typiofunc, str, io->typioparam, typmod, + escontext, &res)) + { + res = (Datum) 0; + } /* free temporary buffer */ if (str != json) @@ -3053,7 +3124,7 @@ populate_domain(DomainIOData *io, res = populate_record_field(io->base_io, io->base_typid, io->base_typmod, colname, mcxt, PointerGetDatum(NULL), - jsv, &isnull); + jsv, &isnull, NULL); Assert(!isnull); } @@ -3158,7 +3229,8 @@ populate_record_field(ColumnIOData *col, MemoryContext mcxt, Datum defaultval, JsValue *jsv, - bool *isnull) + bool *isnull, + Node *escontext) { TypeCat typcat; @@ -3191,10 +3263,12 @@ populate_record_field(ColumnIOData *col, switch (typcat) { case TYPECAT_SCALAR: - return populate_scalar(&col->scalar_io, typid, typmod, jsv); + return populate_scalar(&col->scalar_io, typid, typmod, jsv, + escontext); case TYPECAT_ARRAY: - return populate_array(&col->io.array, colname, mcxt, jsv); + return populate_array(&col->io.array, colname, mcxt, jsv, + escontext, isnull); case TYPECAT_COMPOSITE: case TYPECAT_COMPOSITE_DOMAIN: @@ -3215,6 +3289,53 @@ populate_record_field(ColumnIOData *col, } } +/* recursively populate specified type from a json/jsonb value */ +Datum +json_populate_type(Datum json_val, Oid json_type, Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext) +{ + JsValue jsv = {0}; + JsonbValue jbv; + + jsv.is_json = json_type == JSONOID; + + if (*isnull) + { + if (jsv.is_json) + jsv.val.json.str = NULL; + else + jsv.val.jsonb = NULL; + } + else if (jsv.is_json) + { + text *json = DatumGetTextPP(json_val); + + jsv.val.json.str = VARDATA_ANY(json); + jsv.val.json.len = VARSIZE_ANY_EXHDR(json); + jsv.val.json.type = JSON_TOKEN_INVALID; /* not used in + * populate_composite() */ + } + else + { + Jsonb *jsonb = DatumGetJsonbP(json_val); + + jsv.val.jsonb = &jbv; + + /* fill binary jsonb value pointing to jb */ + jbv.type = jbvBinary; + jbv.val.binary.data = &jsonb->root; + jbv.val.binary.len = VARSIZE(jsonb) - VARHDRSZ; + } + + if (!*cache) + *cache = MemoryContextAllocZero(mcxt, sizeof(ColumnIOData)); + + return populate_record_field(*cache, typid, typmod, NULL, mcxt, + PointerGetDatum(NULL), &jsv, isnull, + escontext); +} + static RecordIOData * allocate_record_info(MemoryContext mcxt, int ncolumns) { @@ -3356,7 +3477,8 @@ populate_record(TupleDesc tupdesc, mcxt, nulls[i] ? (Datum) 0 : values[i], &field, - &nulls[i]); + &nulls[i], + NULL); } res = heap_form_tuple(tupdesc, values, nulls); diff --git a/src/backend/utils/adt/jsonpath.c b/src/backend/utils/adt/jsonpath.c index 7891fde310..5194d0d91f 100644 --- a/src/backend/utils/adt/jsonpath.c +++ b/src/backend/utils/adt/jsonpath.c @@ -68,7 +68,9 @@ #include "libpq/pqformat.h" #include "nodes/miscnodes.h" #include "miscadmin.h" +#include "nodes/nodeFuncs.h" #include "utils/builtins.h" +#include "utils/formatting.h" #include "utils/json.h" #include "utils/jsonpath.h" @@ -1109,3 +1111,256 @@ jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, return true; } + +/* SQL/JSON datatype status: */ +typedef enum JsonPathDatatypeStatus +{ + jpdsNonDateTime, /* null, bool, numeric, string, array, object */ + jpdsUnknownDateTime, /* unknown datetime type */ + jpdsDateTimeZoned, /* timetz, timestamptz */ + jpdsDateTimeNonZoned /* time, timestamp, date */ +} JsonPathDatatypeStatus; + +/* Context for jspIsMutableWalker() */ +typedef struct JsonPathMutableContext +{ + List *varnames; /* list of variable names */ + List *varexprs; /* list of variable expressions */ + JsonPathDatatypeStatus current; /* status of @ item */ + bool lax; /* jsonpath is lax or strict */ + bool mutable; /* resulting mutability status */ +} JsonPathMutableContext; + +/* + * Recursive walker for jspIsMutable() + */ +static JsonPathDatatypeStatus +jspIsMutableWalker(JsonPathItem *jpi, JsonPathMutableContext *cxt) +{ + JsonPathItem next; + JsonPathDatatypeStatus status = jpdsNonDateTime; + + while (!cxt->mutable) + { + JsonPathItem arg; + JsonPathDatatypeStatus leftStatus; + JsonPathDatatypeStatus rightStatus; + + switch (jpi->type) + { + case jpiRoot: + Assert(status == jpdsNonDateTime); + break; + + case jpiCurrent: + Assert(status == jpdsNonDateTime); + status = cxt->current; + break; + + case jpiFilter: + { + JsonPathDatatypeStatus prevStatus = cxt->current; + + cxt->current = status; + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + + cxt->current = prevStatus; + break; + } + + case jpiVariable: + { + int32 len; + const char *name = jspGetString(jpi, &len); + ListCell *lc1; + ListCell *lc2; + + Assert(status == jpdsNonDateTime); + + forboth(lc1, cxt->varnames, lc2, cxt->varexprs) + { + String *varname = lfirst_node(String, lc1); + Node *varexpr = lfirst(lc2); + + if (strncmp(varname->sval, name, len)) + continue; + + switch (exprType(varexpr)) + { + case DATEOID: + case TIMEOID: + case TIMESTAMPOID: + status = jpdsDateTimeNonZoned; + break; + + case TIMETZOID: + case TIMESTAMPTZOID: + status = jpdsDateTimeZoned; + break; + + default: + status = jpdsNonDateTime; + break; + } + + break; + } + break; + } + + case jpiEqual: + case jpiNotEqual: + case jpiLess: + case jpiGreater: + case jpiLessOrEqual: + case jpiGreaterOrEqual: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + leftStatus = jspIsMutableWalker(&arg, cxt); + + jspGetRightArg(jpi, &arg); + rightStatus = jspIsMutableWalker(&arg, cxt); + + /* + * Comparison of datetime type with different timezone status + * is mutable. + */ + if (leftStatus != jpdsNonDateTime && + rightStatus != jpdsNonDateTime && + (leftStatus == jpdsUnknownDateTime || + rightStatus == jpdsUnknownDateTime || + leftStatus != rightStatus)) + cxt->mutable = true; + break; + + case jpiNot: + case jpiIsUnknown: + case jpiExists: + case jpiPlus: + case jpiMinus: + Assert(status == jpdsNonDateTime); + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiAnd: + case jpiOr: + case jpiAdd: + case jpiSub: + case jpiMul: + case jpiDiv: + case jpiMod: + case jpiStartsWith: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + jspGetRightArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiIndexArray: + for (int i = 0; i < jpi->content.array.nelems; i++) + { + JsonPathItem from; + JsonPathItem to; + + if (jspGetArraySubscript(jpi, &from, &to, i)) + jspIsMutableWalker(&to, cxt); + + jspIsMutableWalker(&from, cxt); + } + /* FALLTHROUGH */ + + case jpiAnyArray: + if (!cxt->lax) + status = jpdsNonDateTime; + break; + + case jpiAny: + if (jpi->content.anybounds.first > 0) + status = jpdsNonDateTime; + break; + + case jpiDatetime: + if (jpi->content.arg) + { + char *template; + + jspGetArg(jpi, &arg); + if (arg.type != jpiString) + { + status = jpdsNonDateTime; + break; /* there will be runtime error */ + } + + template = jspGetString(&arg, NULL); + if (datetime_format_has_tz(template)) + status = jpdsDateTimeZoned; + else + status = jpdsDateTimeNonZoned; + } + else + { + status = jpdsUnknownDateTime; + } + break; + + case jpiLikeRegex: + Assert(status == jpdsNonDateTime); + jspInitByBuffer(&arg, jpi->base, jpi->content.like_regex.expr); + jspIsMutableWalker(&arg, cxt); + break; + + /* literals */ + case jpiNull: + case jpiString: + case jpiNumeric: + case jpiBool: + /* accessors */ + case jpiKey: + case jpiAnyKey: + /* special items */ + case jpiSubscript: + case jpiLast: + /* item methods */ + case jpiType: + case jpiSize: + case jpiAbs: + case jpiFloor: + case jpiCeiling: + case jpiDouble: + case jpiKeyValue: + status = jpdsNonDateTime; + break; + } + + if (!jspGetNext(jpi, &next)) + break; + + jpi = &next; + } + + return status; +} + +/* + * Check whether jsonpath expression is immutable or not. + */ +bool +jspIsMutable(JsonPath *path, List *varnames, List *varexprs) +{ + JsonPathMutableContext cxt; + JsonPathItem jpi; + + cxt.varnames = varnames; + cxt.varexprs = varexprs; + cxt.current = jpdsNonDateTime; + cxt.lax = (path->header & JSONPATH_LAX) != 0; + cxt.mutable = false; + + jspInit(&jpi, path); + jspIsMutableWalker(&jpi, &cxt); + + return cxt.mutable; +} diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index 2d0599b4aa..eded22e194 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -87,12 +87,16 @@ typedef struct JsonBaseObjectInfo int id; } JsonBaseObjectInfo; +typedef int (*JsonPathVarCallback) (void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); + /* * Context of jsonpath execution. */ typedef struct JsonPathExecContext { - Jsonb *vars; /* variables to substitute into jsonpath */ + void *vars; /* variables to substitute into jsonpath */ + JsonPathVarCallback getVar; JsonbValue *root; /* for $ evaluation */ JsonbValue *current; /* for @ evaluation */ JsonBaseObjectInfo baseObject; /* "base object" for .keyvalue() @@ -174,7 +178,8 @@ typedef JsonPathBool (*JsonPathPredicateCallback) (JsonPathItem *jsp, void *param); typedef Numeric (*BinaryArithmFunc) (Numeric num1, Numeric num2, bool *error); -static JsonPathExecResult executeJsonPath(JsonPath *path, Jsonb *vars, +static JsonPathExecResult executeJsonPath(JsonPath *path, void *vars, + JsonPathVarCallback getVar, Jsonb *json, bool throwErrors, JsonValueList *result, bool useTz); static JsonPathExecResult executeItem(JsonPathExecContext *cxt, @@ -225,8 +230,13 @@ static JsonPathExecResult appendBoolResult(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonValueList *found, JsonPathBool res); static void getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, JsonbValue *value); +static int GetJsonPathVar(void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); static void getJsonPathVariable(JsonPathExecContext *cxt, - JsonPathItem *variable, Jsonb *vars, JsonbValue *value); + JsonPathItem *variable, JsonbValue *value); +static int getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, + int varNameLen, JsonbValue *val, + JsonbValue *baseObject); static int JsonbArraySize(JsonbValue *jb); static JsonPathBool executeComparison(JsonPathItem *cmp, JsonbValue *lv, JsonbValue *rv, void *p); @@ -284,7 +294,8 @@ jsonb_path_exists_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - res = executeJsonPath(jp, vars, jb, !silent, NULL, tz); + res = executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, NULL, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -339,7 +350,8 @@ jsonb_path_match_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -417,7 +429,8 @@ jsonb_path_query_internal(FunctionCallInfo fcinfo, bool tz) vars = PG_GETARG_JSONB_P_COPY(2); silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); funcctx->user_fctx = JsonValueListGetList(&found); @@ -464,7 +477,8 @@ jsonb_path_query_array_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_RETURN_JSONB_P(JsonbValueToJsonb(wrapItemsInArray(&found))); } @@ -495,7 +509,8 @@ jsonb_path_query_first_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); if (JsonValueListLength(&found) >= 1) PG_RETURN_JSONB_P(JsonbValueToJsonb(JsonValueListHead(&found))); @@ -537,8 +552,9 @@ jsonb_path_query_first_tz(PG_FUNCTION_ARGS) * In other case it tries to find all the satisfied result items. */ static JsonPathExecResult -executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, - JsonValueList *result, bool useTz) +executeJsonPath(JsonPath *path, void *vars, JsonPathVarCallback getVar, + Jsonb *json, bool throwErrors, JsonValueList *result, + bool useTz) { JsonPathExecContext cxt; JsonPathExecResult res; @@ -550,22 +566,16 @@ executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, if (!JsonbExtractScalar(&json->root, &jbv)) JsonbInitBinary(&jbv, json); - if (vars && !JsonContainerIsObject(&vars->root)) - { - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"vars\" argument is not an object"), - errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); - } - cxt.vars = vars; + cxt.getVar = getVar; cxt.laxMode = (path->header & JSONPATH_LAX) != 0; cxt.ignoreStructuralErrors = cxt.laxMode; cxt.root = &jbv; cxt.current = &jbv; cxt.baseObject.jbc = NULL; cxt.baseObject.id = 0; - cxt.lastGeneratedObjectId = vars ? 2 : 1; + /* 1 + number of base objects in vars */ + cxt.lastGeneratedObjectId = 1 + getVar(vars, NULL, 0, NULL, NULL); cxt.innermostArraySize = -1; cxt.throwErrors = throwErrors; cxt.useTz = useTz; @@ -2108,54 +2118,118 @@ getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, &value->val.string.len); break; case jpiVariable: - getJsonPathVariable(cxt, item, cxt->vars, value); + getJsonPathVariable(cxt, item, value); return; default: elog(ERROR, "unexpected jsonpath item type"); } } +/* + * Returns the computed value of a JSON path variable with given name. + */ +static int +GetJsonPathVar(void *cxt, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject) +{ + JsonPathVariable *var = NULL; + List *vars = cxt; + ListCell *lc; + int id = 1; + + if (!varName) + return list_length(vars); + + foreach(lc, vars) + { + JsonPathVariable *curvar = lfirst(lc); + + if (!strncmp(curvar->name, varName, varNameLen)) + { + var = curvar; + break; + } + + id++; + } + + if (!var) + return -1; + + if (var->isnull) + { + val->type = jbvNull; + return 0; + } + + JsonItemFromDatum(var->value, var->typid, var->typmod, val); + + *baseObject = *val; + return id; +} + /* * Get the value of variable passed to jsonpath executor */ static void getJsonPathVariable(JsonPathExecContext *cxt, JsonPathItem *variable, - Jsonb *vars, JsonbValue *value) + JsonbValue *value) { char *varName; int varNameLength; + JsonbValue baseObject; + int baseObjectId; + + Assert(variable->type == jpiVariable); + varName = jspGetString(variable, &varNameLength); + + if (!cxt->vars || + (baseObjectId = cxt->getVar(cxt->vars, varName, varNameLength, value, + &baseObject)) < 0) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("could not find jsonpath variable \"%s\"", + pnstrdup(varName, varNameLength)))); + + if (baseObjectId > 0) + setBaseObject(cxt, &baseObject, baseObjectId); +} + +static int +getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, int varNameLength, + JsonbValue *value, JsonbValue *baseObject) +{ + Jsonb *vars = varsJsonb; JsonbValue tmp; JsonbValue *v; - if (!vars) + if (!varName) { - value->type = jbvNull; - return; + if (vars && !JsonContainerIsObject(&vars->root)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"vars\" argument is not an object"), + errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); + } + + return vars ? 1 : 0; /* count of base objects */ } - Assert(variable->type == jpiVariable); - varName = jspGetString(variable, &varNameLength); tmp.type = jbvString; tmp.val.string.val = varName; tmp.val.string.len = varNameLength; v = findJsonbValueFromContainer(&vars->root, JB_FOBJECT, &tmp); - if (v) - { - *value = *v; - pfree(v); - } - else - { - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("could not find jsonpath variable \"%s\"", - pnstrdup(varName, varNameLength)))); - } + if (!v) + return -1; - JsonbInitBinary(&tmp, vars); - setBaseObject(cxt, &tmp, 1); + *value = *v; + pfree(v); + + JsonbInitBinary(baseObject, vars); + return 1; } /**************** Support functions for JsonPath execution *****************/ @@ -2812,3 +2886,240 @@ compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, return DatumGetInt32(DirectFunctionCall2(cmpfunc, val1, val2)); } + +/* Executor-callable JSON_EXISTS implementation */ +bool +JsonPathExists(Datum jb, JsonPath *jp, List *vars, bool *error) +{ + JsonPathExecResult res = executeJsonPath(jp, vars, GetJsonPathVar, + DatumGetJsonbP(jb), !error, NULL, + true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + *error = true; + + return res == jperOk; +} + +/* Executor-callable JSON_QUERY implementation */ +Datum +JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, bool *empty, + bool *error, List *vars) +{ + JsonbValue *first; + bool wrap; + JsonValueList found = {0}; + JsonPathExecResult res PG_USED_FOR_ASSERTS_ONLY; + int count; + + res = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + { + *error = true; + *empty = false; + return (Datum) 0; + } + + count = JsonValueListLength(&found); + + first = count ? JsonValueListHead(&found) : NULL; + + if (!first) + wrap = false; + else if (wrapper == JSW_NONE) + wrap = false; + else if (wrapper == JSW_UNCONDITIONAL) + wrap = true; + else if (wrapper == JSW_CONDITIONAL) + wrap = count > 1 || + IsAJsonbScalar(first) || + (first->type == jbvBinary && + JsonContainerIsScalar(first->val.binary.data)); + else + { + elog(ERROR, "unrecognized json wrapper %d", wrapper); + wrap = false; + } + + if (wrap) + return JsonbPGetDatum(JsonbValueToJsonb(wrapItemsInArray(&found))); + + if (count > 1) + { + if (error) + { + *error = true; + return (Datum) 0; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_QUERY should return singleton item without wrapper"), + errhint("Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array."))); + } + + if (first) + return JsonbPGetDatum(JsonbValueToJsonb(first)); + + *empty = true; + return PointerGetDatum(NULL); +} + +/* Executor-callable JSON_VALUE implementation */ +JsonbValue * +JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars) +{ + JsonbValue *res; + JsonValueList found = {0}; + JsonPathExecResult jper PG_USED_FOR_ASSERTS_ONLY; + int count; + + jper = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(jper)); + + if (error && jperIsError(jper)) + { + *error = true; + *empty = false; + return NULL; + } + + count = JsonValueListLength(&found); + + *empty = !count; + + if (*empty) + return NULL; + + if (count > 1) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + res = JsonValueListHead(&found); + + if (res->type == jbvBinary && + JsonContainerIsScalar(res->val.binary.data)) + JsonbExtractScalar(res->val.binary.data, res); + + if (!IsAJsonbScalar(res)) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_SCALAR_REQUIRED), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + if (res->type == jbvNull) + return NULL; + + return res; +} + +static void +JsonbValueInitNumericDatum(JsonbValue *jbv, Datum num) +{ + jbv->type = jbvNumeric; + jbv->val.numeric = DatumGetNumeric(num); +} + +void +JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) +{ + switch (typid) + { + case BOOLOID: + res->type = jbvBool; + res->val.boolean = DatumGetBool(val); + break; + case NUMERICOID: + JsonbValueInitNumericDatum(res, val); + break; + case INT2OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int2_numeric, val)); + break; + case INT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int4_numeric, val)); + break; + case INT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int8_numeric, val)); + break; + case FLOAT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float4_numeric, val)); + break; + case FLOAT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float8_numeric, val)); + break; + case TEXTOID: + case VARCHAROID: + res->type = jbvString; + res->val.string.val = VARDATA_ANY(val); + res->val.string.len = VARSIZE_ANY_EXHDR(val); + break; + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + res->type = jbvDatetime; + res->val.datetime.value = val; + res->val.datetime.typid = typid; + res->val.datetime.typmod = typmod; + res->val.datetime.tz = 0; + break; + case JSONBOID: + { + JsonbValue *jbv = res; + Jsonb *jb = DatumGetJsonbP(val); + + if (JsonContainerIsScalar(&jb->root)) + { + bool result PG_USED_FOR_ASSERTS_ONLY; + + result = JsonbExtractScalar(&jb->root, jbv); + Assert(result); + } + else + JsonbInitBinary(jbv, jb); + break; + } + case JSONOID: + { + text *txt = DatumGetTextP(val); + char *str = text_to_cstring(txt); + Jsonb *jb; + + jb = DatumGetJsonbP(DirectFunctionCall1(jsonb_in, + CStringGetDatum(str))); + pfree(str); + + JsonItemFromDatum(JsonbPGetDatum(jb), JSONBOID, -1, res); + break; + } + default: + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); + } +} diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d1b03d6cb2..d1ec418ccf 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -476,6 +476,8 @@ static void get_const_expr(Const *constval, deparse_context *context, int showtype); static void get_const_collation(Const *constval, deparse_context *context); static void get_json_format(JsonFormat *format, StringInfo buf); +static void get_json_returning(JsonReturning *returning, StringInfo buf, + bool json_format_by_default); static void get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, bool showimplicit); static void get_json_constructor_options(JsonConstructorExpr *ctor, @@ -518,6 +520,8 @@ static char *generate_qualified_type_name(Oid typid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); +static void get_json_path_spec(Node *path_spec, deparse_context *context, + bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8279,6 +8283,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_WindowFunc: case T_FuncExpr: case T_JsonConstructorExpr: + case T_JsonExpr: /* function-like: name(..) or name[..] */ return true; @@ -8450,6 +8455,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_GroupingFunc: /* own parentheses */ case T_WindowFunc: /* own parentheses */ case T_CaseExpr: /* other separators */ + case T_JsonExpr: /* own parentheses */ return true; default: return false; @@ -8565,6 +8571,65 @@ get_rule_expr_paren(Node *node, deparse_context *context, appendStringInfoChar(context->buf, ')'); } +static void +get_json_behavior(JsonBehavior *behavior, deparse_context *context, + const char *on) +{ + /* + * The order of array elements must correspond to the order of + * JsonBehaviorType members. + */ + const char *behavior_names[] = + { + " NULL", + " ERROR", + " EMPTY", + " TRUE", + " FALSE", + " UNKNOWN", + " EMPTY ARRAY", + " EMPTY OBJECT", + " DEFAULT " + }; + + if ((int) behavior->btype < 0 || behavior->btype >= lengthof(behavior_names)) + elog(ERROR, "invalid json behavior type: %d", behavior->btype); + + appendStringInfoString(context->buf, behavior_names[behavior->btype]); + + if (behavior->btype == JSON_BEHAVIOR_DEFAULT) + get_rule_expr(behavior->default_expr, context, false); + + appendStringInfo(context->buf, " ON %s", on); +} + +/* + * get_json_expr_options + * + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + */ +static void +get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, + JsonBehaviorType default_behavior) +{ + if (jsexpr->op == JSON_QUERY_OP) + { + if (jsexpr->wrapper == JSW_CONDITIONAL) + appendStringInfo(context->buf, " WITH CONDITIONAL WRAPPER"); + else if (jsexpr->wrapper == JSW_UNCONDITIONAL) + appendStringInfo(context->buf, " WITH UNCONDITIONAL WRAPPER"); + + if (jsexpr->omit_quotes) + appendStringInfo(context->buf, " OMIT QUOTES"); + } + + if (jsexpr->op != JSON_EXISTS_OP && + jsexpr->on_empty->btype != default_behavior) + get_json_behavior(jsexpr->on_empty, context, "EMPTY"); + + if (jsexpr->on_error->btype != default_behavior) + get_json_behavior(jsexpr->on_error, context, "ERROR"); +} /* ---------- * get_rule_expr - Parse back an expression @@ -9724,6 +9789,7 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonValueExpr: { JsonValueExpr *jve = (JsonValueExpr *) node; @@ -9773,6 +9839,63 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + appendStringInfoString(buf, "JSON_QUERY("); + break; + case JSON_VALUE_OP: + appendStringInfoString(buf, "JSON_VALUE("); + break; + case JSON_EXISTS_OP: + appendStringInfoString(buf, "JSON_EXISTS("); + break; + } + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_json_path_spec(jexpr->path_spec, context, showimplicit); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoString(buf, " PASSING "); + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + get_rule_expr((Node *) lfirst(lc2), context, showimplicit); + appendStringInfo(buf, " AS %s", + ((String *) lfirst_node(String, lc1))->sval); + } + } + + if (jexpr->op != JSON_EXISTS_OP || + jexpr->returning->typid != BOOLOID) + get_json_returning(jexpr->returning, context->buf, + jexpr->op == JSON_QUERY_OP); + + get_json_expr_options(jexpr, context, + jexpr->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : JSON_BEHAVIOR_NULL); + + appendStringInfoString(buf, ")"); + } + break; + case T_List: { char *sep; @@ -9896,6 +10019,7 @@ looks_like_function(Node *node) case T_MinMaxExpr: case T_SQLValueFunction: case T_XmlExpr: + case T_JsonExpr: /* these are all accepted by func_expr_common_subexpr */ return true; default: @@ -10755,6 +10879,18 @@ get_const_collation(Const *constval, deparse_context *context) } } +/* + * get_json_path_spec - Parse back a JSON path specification + */ +static void +get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit) +{ + if (IsA(path_spec, Const)) + get_const_expr((Const *) path_spec, context, -1); + else + get_rule_expr(path_spec, context, showimplicit); +} + /* * get_json_format - Parse back a JsonFormat node */ diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index 048573c2bc..69fb577850 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -16,12 +16,15 @@ #include "executor/nodeAgg.h" #include "nodes/execnodes.h" +#include "nodes/miscnodes.h" /* forward references to avoid circularity */ struct ExprEvalStep; struct SubscriptingRefState; struct ScalarArrayOpExprHashTable; struct JsonConstructorExprState; +struct JsonbValue; +struct JsonExprState; /* Bits in ExprState->flags (see also execnodes.h for public flag bits): */ /* expression's interpreter has been initialized */ @@ -238,6 +241,11 @@ typedef enum ExprEvalOp EEOP_XMLEXPR, EEOP_JSON_CONSTRUCTOR, EEOP_IS_JSON, + EEOP_JSONEXPR_SKIP, + EEOP_JSONEXPR_PATH, + EEOP_JSONEXPR_BEHAVIOR, + EEOP_JSONEXPR_COERCION, + EEOP_JSONEXPR_COERCION_FINISH, EEOP_AGGREF, EEOP_GROUPING_FUNC, EEOP_WINDOW_FUNC, @@ -689,6 +697,57 @@ typedef struct ExprEvalStep JsonIsPredicate *pred; /* original expression node */ } is_json; + /* for EEOP_JSONEXPR_PATH */ + struct + { + struct JsonExprState *jsestate; + } jsonexpr; + + /* for EEOP_JSONEXPR_SKIP */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprSkip() */ + int jump_coercion; + int jump_passing_args; + } jsonexpr_skip; + + /* for EEOP_JSONEXPR_BEHAVIOR */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprBehavior() */ + int jump_onerror_expr; + int jump_onempty_expr; + int jump_coercion; + int jump_skip_coercion; + } jsonexpr_behavior; + + /* for EEOP_JSONEXPR_COERCION */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion; + + /* for EEOP_JSONEXPR_COERCION_FINISH */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion_finish; } d; } ExprEvalStep; @@ -752,6 +811,111 @@ typedef struct JsonConstructorExprState int nargs; } JsonConstructorExprState; +/* + * Information computed before evaluating a JsonExpr expression. + */ +typedef struct JsonExprPreEvalState +{ + /* value/isnull for JsonExpr.formatted_expr */ + NullableDatum formatted_expr; + + /* value/isnull for JsonExpr.pathspec */ + NullableDatum pathspec; + + /* JsonPathVariable entries for JsonExpr.passing_values */ + List *args; +} JsonExprPreEvalState; + +/* + * State for a given member of JsonItemCoercions. + */ +typedef struct JsonCoercionState +{ + /* Expression used to evaluate the coercion */ + JsonCoercion *coercion; + + /* ExprEvalStep to compute this coercion's expression */ + int jump_eval_expr; +} JsonCoercionState; + +/* + * State for evaluating the coercion for a given JSON item using one of + * the following coercions. + * + * Note that while ExecInitExprRec() for JsonItemCoercions will initialize + * ExprEvalSteps for all of the members that need it, only one will get run + * during a given evaluation of the enclosing JsonExpr depending on the type + * of the result JSON item. + */ +typedef struct JsonItemCoercionsState +{ + JsonCoercionState *null; + JsonCoercionState *string; + JsonCoercionState *numeric; + JsonCoercionState *boolean; + JsonCoercionState *date; + JsonCoercionState *time; + JsonCoercionState *timetz; + JsonCoercionState *timestamp; + JsonCoercionState *timestamptz; + JsonCoercionState *composite; +} JsonItemCoercionsState; + +/* + * State for some post-JsonExpr-evaluation processing steps that gets filled + * in JsonExpr evaluation. + */ +typedef struct JsonExprPostEvalState +{ + /* Is JSON item empty? */ + bool empty; + + /* Did JSON item evaluation cause an error? */ + bool error; + + /* Cache for json_populate_type() called for coercion in some cases */ + void *cache; + + /* + * State for evaluating a JSON item coercion. Points to one of those in + * JsonExprState.coercions chosen by ExecPrepareJsonItemCoercion(). + */ + JsonCoercionState *item_jcstate; + bool coercion_error; + bool coercion_done; + + ErrorSaveContext escontext; +} JsonExprPostEvalState; + +/* EEOP_JSONEXPR state, too big to inline */ +typedef struct JsonExprState +{ + /* original expression node */ + JsonExpr *jsexpr; + + /* + * Should errors be thrown or handled softly? Different parts of + * evaluating a given JsonExpr may require different treatment depending + * on the JsonExprOp; see ExecEvalJsonExprBehavior(). + */ + bool throw_error; + + JsonExprPreEvalState pre_eval; + JsonExprPostEvalState post_eval; + + struct + { + FmgrInfo *finfo; /* typinput function for output type */ + Oid typioparam; + } input; /* I/O info for output type */ + + /* + * Either of the following two is used by ExecEvalJsonExprCoercion() to + * apply coercion to the final result if needed. + */ + JsonCoercionState *result_jcstate; + JsonItemCoercionsState *item_jcstates; +} JsonExprState; /* functions in execExpr.c */ extern void ExprEvalPushStep(ExprState *es, const ExprEvalStep *s); @@ -805,6 +969,14 @@ extern void ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op); extern void ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull); +extern int ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op); +extern void ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, + ExprContext *econtext); extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op); extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op, ExprContext *econtext); diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index ac02247947..089d1c5750 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -17,6 +17,7 @@ #include "executor/execdesc.h" #include "fmgr.h" #include "nodes/lockoptions.h" +#include "nodes/miscnodes.h" #include "nodes/parsenodes.h" #include "utils/memutils.h" diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index cb714f4a19..584bf7001a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -129,6 +129,9 @@ typedef struct ExprState Datum *innermost_domainval; bool *innermost_domainnull; + + /* ErrorSaveContext for soft-error capture. */ + Node *escontext; } ExprState; diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 06d991b725..5e149b0266 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -111,6 +111,7 @@ extern VacuumRelation *makeVacuumRelation(RangeVar *relation, Oid oid, List *va_ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *expr, JsonFormat *format); +extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index d926713bd9..7d63a37d5f 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1727,6 +1727,12 @@ typedef enum JsonQuotes JS_QUOTES_OMIT /* OMIT QUOTES */ } JsonQuotes; +/* + * JsonPathSpec - + * representation of JSON path constant + */ +typedef char *JsonPathSpec; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1738,6 +1744,48 @@ typedef struct JsonOutput JsonReturning *returning; /* RETURNING FORMAT clause and type Oids */ } JsonOutput; +/* + * JsonArgument - + * representation of argument from JSON PASSING clause + */ +typedef struct JsonArgument +{ + NodeTag type; + JsonValueExpr *val; /* argument value expression */ + char *name; /* argument name */ +} JsonArgument; + +/* + * JsonCommon - + * representation of common syntax of functions using JSON path + */ +typedef struct JsonCommon +{ + NodeTag type; + JsonValueExpr *expr; /* context item expression */ + Node *pathspec; /* JSON path specification expression */ + char *pathname; /* path name, if any */ + List *passing; /* list of PASSING clause arguments, if any */ + int location; /* token location, or -1 if unknown */ +} JsonCommon; + +/* + * JsonFuncExpr - + * untransformed representation of JSON function expressions + */ +typedef struct JsonFuncExpr +{ + NodeTag type; + JsonExprOp op; /* expression type */ + JsonCommon *common; /* common syntax */ + JsonOutput *output; /* output clause, if specified */ + JsonBehavior *on_empty; /* ON EMPTY behavior, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + JsonWrapper wrapper; /* array wrapper behavior (JSON_QUERY only) */ + bool omit_quotes; /* omit or keep quotes? (JSON_QUERY only) */ + int location; /* token location, or -1 if unknown */ +} JsonFuncExpr; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6c6a1810af..0e5a160c90 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1542,6 +1542,17 @@ typedef struct XmlExpr int location; } XmlExpr; +/* + * JsonExprOp - + * enumeration of JSON functions using JSON path + */ +typedef enum JsonExprOp +{ + JSON_VALUE_OP, /* JSON_VALUE() */ + JSON_QUERY_OP, /* JSON_QUERY() */ + JSON_EXISTS_OP /* JSON_EXISTS() */ +} JsonExprOp; + /* * JsonEncoding - * representation of JSON ENCODING clause @@ -1566,6 +1577,37 @@ typedef enum JsonFormatType * jsonb */ } JsonFormatType; +/* + * JsonBehaviorType - + * enumeration of behavior types used in JSON ON ... BEHAVIOR clause + * + * If enum members are reordered, get_json_behavior() from ruleutils.c + * must be updated accordingly. + */ +typedef enum JsonBehaviorType +{ + JSON_BEHAVIOR_NULL = 0, + JSON_BEHAVIOR_ERROR, + JSON_BEHAVIOR_EMPTY, + JSON_BEHAVIOR_TRUE, + JSON_BEHAVIOR_FALSE, + JSON_BEHAVIOR_UNKNOWN, + JSON_BEHAVIOR_EMPTY_ARRAY, + JSON_BEHAVIOR_EMPTY_OBJECT, + JSON_BEHAVIOR_DEFAULT +} JsonBehaviorType; + +/* + * JsonWrapper - + * representation of WRAPPER clause for JSON_QUERY() + */ +typedef enum JsonWrapper +{ + JSW_NONE, + JSW_CONDITIONAL, + JSW_UNCONDITIONAL, +} JsonWrapper; + /* * JsonFormat - * representation of JSON FORMAT clause @@ -1656,6 +1698,73 @@ typedef struct JsonIsPredicate int location; /* token location, or -1 if unknown */ } JsonIsPredicate; +/* + * JsonBehavior - + * representation of JSON ON ... BEHAVIOR clause + */ +typedef struct JsonBehavior +{ + NodeTag type; + JsonBehaviorType btype; /* behavior type */ + Node *default_expr; /* default expression, if any */ +} JsonBehavior; + +/* + * JsonCoercion - + * coercion from SQL/JSON item types to SQL types + */ +typedef struct JsonCoercion +{ + NodeTag type; + Node *expr; /* resulting expression coerced to target type */ + bool via_populate; /* coerce result using json_populate_type()? */ + bool via_io; /* coerce result using type input function? */ + Oid collation; /* collation for coercion via I/O or populate */ +} JsonCoercion; + +/* + * JsonItemCoercions - + * expressions for coercion from SQL/JSON item types directly to the + * output SQL type + */ +typedef struct JsonItemCoercions +{ + NodeTag type; + JsonCoercion *null; + JsonCoercion *string; + JsonCoercion *numeric; + JsonCoercion *boolean; + JsonCoercion *date; + JsonCoercion *time; + JsonCoercion *timetz; + JsonCoercion *timestamp; + JsonCoercion *timestamptz; + JsonCoercion *composite; /* arrays and objects */ +} JsonItemCoercions; + +/* + * JsonExpr - + * transformed representation of JSON_VALUE(), JSON_QUERY(), JSON_EXISTS() + */ +typedef struct JsonExpr +{ + Expr xpr; + JsonExprOp op; /* json function ID */ + Node *formatted_expr; /* formatted context item expression */ + JsonCoercion *result_coercion; /* resulting coercion to RETURNING type */ + JsonFormat *format; /* context item format (JSON/JSONB) */ + Node *path_spec; /* JSON path specification expression */ + List *passing_names; /* PASSING argument names */ + List *passing_values; /* PASSING argument values */ + JsonReturning *returning; /* RETURNING clause type/format info */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + JsonItemCoercions *coercions; /* coercions for JSON_VALUE */ + JsonWrapper wrapper; /* WRAPPER for JSON_QUERY */ + bool omit_quotes; /* KEEP/OMIT QUOTES for JSON_QUERY */ + int location; /* token location, or -1 if unknown */ +} JsonExpr; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 5984dcfa4b..0954d9fc7b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -93,6 +93,7 @@ PG_KEYWORD("commit", COMMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("committed", COMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("compression", COMPRESSION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("concurrently", CONCURRENTLY, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("conditional", CONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("configuration", CONFIGURATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("conflict", CONFLICT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("connection", CONNECTION, UNRESERVED_KEYWORD, BARE_LABEL) @@ -147,11 +148,13 @@ PG_KEYWORD("double", DOUBLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("drop", DROP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("each", EACH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("empty", EMPTY_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("event", EVENT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("except", EXCEPT, RESERVED_KEYWORD, AS_LABEL) @@ -233,10 +236,14 @@ PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_exists", JSON_EXISTS, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) @@ -302,6 +309,7 @@ PG_KEYWORD("off", OFF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("offset", OFFSET, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("oids", OIDS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("old", OLD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("omit", OMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("on", ON, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("only", ONLY, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("operator", OPERATOR, UNRESERVED_KEYWORD, BARE_LABEL) @@ -344,6 +352,7 @@ PG_KEYWORD("procedures", PROCEDURES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("publication", PUBLICATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("quotes", QUOTES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("range", RANGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("read", READ, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("real", REAL, COL_NAME_KEYWORD, BARE_LABEL) @@ -414,6 +423,7 @@ PG_KEYWORD("stdout", STDOUT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("storage", STORAGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("stored", STORED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("string", STRING_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("subscription", SUBSCRIPTION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("substring", SUBSTRING, COL_NAME_KEYWORD, BARE_LABEL) @@ -449,6 +459,7 @@ PG_KEYWORD("types", TYPES_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uescape", UESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unbounded", UNBOUNDED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uncommitted", UNCOMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("unconditional", UNCONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unencrypted", UNENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("union", UNION, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("unique", UNIQUE, RESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/formatting.h b/src/include/utils/formatting.h index 0cad3a2709..d4ca0f42ff 100644 --- a/src/include/utils/formatting.h +++ b/src/include/utils/formatting.h @@ -17,7 +17,6 @@ #ifndef _FORMATTING_H_ #define _FORMATTING_H_ - extern char *str_tolower(const char *buff, size_t nbytes, Oid collid); extern char *str_toupper(const char *buff, size_t nbytes, Oid collid); extern char *str_initcap(const char *buff, size_t nbytes, Oid collid); @@ -29,5 +28,6 @@ extern char *asc_initcap(const char *buff, size_t nbytes); extern Datum parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, Oid *typid, int32 *typmod, int *tz, struct Node *escontext); +extern bool datetime_format_has_tz(const char *fmt_str); #endif diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index 8417a74298..d9e28d14ce 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -439,6 +439,9 @@ extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, int estimated_len); +extern Jsonb *JsonbMakeEmptyArray(void); +extern Jsonb *JsonbMakeEmptyObject(void); +extern char *JsonbUnquote(Jsonb *jb); extern bool JsonbExtractScalar(JsonbContainer *jbc, JsonbValue *res); extern const char *JsonbTypeName(JsonbValue *val); diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index a85203d4a4..cd9cc1480e 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -15,6 +15,7 @@ #define JSONFUNCS_H #include "common/jsonapi.h" +#include "nodes/nodes.h" #include "utils/jsonb.h" /* @@ -63,4 +64,9 @@ extern Jsonb *transform_jsonb_string_values(Jsonb *jsonb, void *action_state, extern text *transform_json_string_values(text *json, void *action_state, JsonTransformStringValuesAction transform_action); +extern Datum json_populate_type(Datum json_val, Oid json_type, + Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext); + #endif diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index f0181e045f..4dae78a98c 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -16,7 +16,9 @@ #include "fmgr.h" #include "nodes/pg_list.h" +#include "nodes/primnodes.h" #include "utils/jsonb.h" +#include "utils/jsonfuncs.h" typedef struct { @@ -184,6 +186,7 @@ extern bool jspGetBool(JsonPathItem *v); extern char *jspGetString(JsonPathItem *v, int32 *len); extern bool jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, int i); +extern bool jspIsMutable(JsonPath *path, List *varnames, List *varexprs); extern const char *jspOperationName(JsonPathItemType type); @@ -261,4 +264,28 @@ extern bool jspConvertRegexFlags(uint32 xflags, int *result, struct Node *escontext); +/* + * Evaluation of jsonpath + */ + +/* External variable passed into jsonpath. */ +typedef struct JsonPathVariable +{ + char *name; + Oid typid; + int32 typmod; + Datum value; + bool isnull; +} JsonPathVariable; + +/* SQL/JSON item */ +extern void JsonItemFromDatum(Datum val, Oid typid, int32 typmod, + JsonbValue *res); + +extern bool JsonPathExists(Datum jb, JsonPath *path, List *vars, bool *error); +extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, + bool *empty, bool *error, List *vars); +extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, + bool *error, List *vars); + #endif diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer index 435c139ec2..b2aa44f36d 100644 --- a/src/interfaces/ecpg/preproc/ecpg.trailer +++ b/src/interfaces/ecpg/preproc/ecpg.trailer @@ -651,6 +651,34 @@ var_type: simple_type $$.type_index = mm_strdup("-1"); $$.type_sizeof = NULL; } + | STRING_P + { + if (INFORMIX_MODE) + { + /* In Informix mode, "string" is automatically a typedef */ + $$.type_enum = ECPGt_string; + $$.type_str = mm_strdup("char"); + $$.type_dimension = mm_strdup("-1"); + $$.type_index = mm_strdup("-1"); + $$.type_sizeof = NULL; + } + else + { + /* Otherwise, legal only if user typedef'ed it */ + struct typedefs *this = get_typedef("string", false); + + $$.type_str = (this->type->type_enum == ECPGt_varchar || this->type->type_enum == ECPGt_bytea) ? EMPTY : mm_strdup(this->name); + $$.type_enum = this->type->type_enum; + $$.type_dimension = this->type->type_dimension; + $$.type_index = this->type->type_index; + if (this->type->type_sizeof && strlen(this->type->type_sizeof) != 0) + $$.type_sizeof = this->type->type_sizeof; + else + $$.type_sizeof = cat_str(3, mm_strdup("sizeof("), mm_strdup(this->name), mm_strdup(")")); + + struct_member_list[struct_level] = ECPGstruct_member_dup(this->struct_member_list); + } + } | INTERVAL ecpg_interval { $$.type_enum = ECPGt_interval; diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out new file mode 100644 index 0000000000..8b87580752 --- /dev/null +++ b/src/test/regress/expected/json_sqljson.out @@ -0,0 +1,18 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); +ERROR: JSON_EXISTS() is not yet implemented for the json type +LINE 1: SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_VALUE +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); +ERROR: JSON_VALUE() is not yet implemented for the json type +LINE 1: SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_QUERY +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); +ERROR: JSON_QUERY() is not yet implemented for the json type +LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out new file mode 100644 index 0000000000..22f8679917 --- /dev/null +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -0,0 +1,1042 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL::jsonb, '$'); + json_exists +------------- + +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb 'null', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_EXISTS(jsonb 'null', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{}', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + json_exists +------------- + f +(1 row) + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + json_exists +------------- + t +(1 row) + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); + json_exists +------------- + 1 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); + json_exists +------------- + 0 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); + json_exists +------------- + true +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +ERROR: cannot cast type boolean to jsonb +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +ERROR: cannot cast type boolean to real +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_EXISTS() +LINE 1: ...CT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSO... + ^ +-- JSON_VALUE +SELECT JSON_VALUE(NULL::jsonb, '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$'); + json_value +------------ + true +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + json_value +------------ + t +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); + json_value +------------ + 123 +(1 row) + +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); +ERROR: SQL/JSON item cannot be cast to target type +SELECT JSON_VALUE(jsonb '1.23', '$'); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "1.23" +SELECT JSON_VALUE(jsonb '"aaa"', '$'); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); + json_value +------------ + aa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "aaa" +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); + json_value +------------ + 111 +(1 row) + +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + ?column? +------------ + 03-01-2017 +(1 row) + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '[]', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '{}', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '1', '$.a'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); + json_value +------------ + error +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 3 +(1 row) + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); + json_value +------------ + 0 +(1 row) + +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: " " +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 5 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_VALUE() +LINE 1: ...CT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSO... + ^ +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + x | y +---+---- + 0 | -2 + 1 | 2 + 2 | -1 +(3 rows) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); + json_value +------------ + (1,2) +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + json_value +------------ + (1,2) +(1 row) + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); + json_value +-------------------------- + Tue Feb 20 18:34:56 2018 +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- JSON_QUERY +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + json_query | json_query | json_query | json_query | json_query +--------------------+--------------------+--------------------+----------------------+---------------------- + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, null, "2"] | [1, null, "2"] | [1, null, "2"] | [[1, null, "2"]] | [[1, null, "2"]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] +(6 rows) + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + unspec | without | with cond | with uncond | with +--------------------+--------------------+---------------------+----------------------+---------------------- + | | | | + | | | | + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, 2, 3] | [1, 2, 3] | [1, 2, 3] | [[1, 2, 3]] | [[1, 2, 3]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] + | | [1, "2", null, [3]] | [1, "2", null, [3]] | [1, "2", null, [3]] +(9 rows) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + json_query +------------ + \x616161 +(1 row) + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTE... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTE... + ^ +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_QUERY should return singleton item without wrapper +HINT: Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array. +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); + json_query +------------ + [1, +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); +ERROR: expected JSON array +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + x | y | list +---+---+-------------- + 0 | 0 | [] + 0 | 1 | [1] + 0 | 2 | [1, 2] + 0 | 3 | [1, 2, 3] + 0 | 4 | [1, 2, 3, 4] + 1 | 0 | [] + 1 | 1 | [1] + 1 | 2 | [1, 2] + 1 | 3 | [1, 2, 3] + 1 | 4 | [1, 2, 3, 4] + 2 | 0 | [] + 2 | 1 | [] + 2 | 2 | [2] + 2 | 3 | [2, 3] + 2 | 4 | [2, 3, 4] + 3 | 0 | [] + 3 | 1 | [] + 3 | 2 | [] + 3 | 3 | [3] + 3 | 4 | [3, 4] + 4 | 0 | [] + 4 | 1 | [] + 4 | 2 | [] + 4 | 3 | [] + 4 | 4 | [4] +(25 rows) + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); + json_query +----------------------------------------------------- + (1,aaa,"[1, ""2"", {}]","{""x"": [1, ""2"", {}]}",) +(1 row) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); + unnest +------------------------ + {"a": 1, "b": ["foo"]} + {"a": 2, "c": {}} + 123 +(3 rows) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); + json_query +-------------- + {1,2,NULL,3} +(1 row) + +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); + json_query +------------ + 1 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- Test constraints +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); +\d test_jsonb_constraints + Table "public.test_jsonb_constraints" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+-------------------------------------------------------------------------------- + js | text | | | + i | integer | | | + x | jsonb | | | JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +Check constraints: + "test_jsonb_constraint1" CHECK (js IS JSON) + "test_jsonb_constraint2" CHECK (JSON_EXISTS(js::jsonb, '$."a"' PASSING i + 5 AS int, i::text AS txt, ARRAY[1, 2, 3] AS arr)) + "test_jsonb_constraint3" CHECK (JSON_VALUE(js::jsonb, '$."a"' RETURNING integer DEFAULT ('12'::text || i)::integer ON EMPTY ERROR ON ERROR) > i) + "test_jsonb_constraint4" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb) + "test_jsonb_constraint5" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C")) + "test_jsonb_constraint6" CHECK (JSON_EXISTS(js::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2) + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + check_clause +-------------------------------------------------------------------------------------------------------------------------- + ((JSON_EXISTS((js)::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2)) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C"))) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb)) + ((JSON_VALUE((js)::jsonb, '$."a"' RETURNING integer DEFAULT (('12'::text || i))::integer ON EMPTY ERROR ON ERROR) > i)) + ((js IS JSON)) + (JSON_EXISTS((js)::jsonb, '$."a"' PASSING (i + 5) AS int, (i)::text AS txt, ARRAY[1, 2, 3] AS arr)) +(6 rows) + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +WHERE constraint_name LIKE 'test_jsonb_constraint%'; +ERROR: syntax error at or near "WHERE" +LINE 1: WHERE constraint_name LIKE 'test_jsonb_constraint%'; + ^ +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint1" +DETAIL: Failing row contains (, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains (1, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('[]'); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ([], null, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ({"b": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint3" +DETAIL: Failing row contains ({"a": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint5" +DETAIL: Failing row contains ({"a": 7}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint4" +DETAIL: Failing row contains ({"a": 10}, 1, [1, 2]). +DROP TABLE test_jsonb_constraints; +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index cf46fa3359..cb3230f4dd 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -103,7 +103,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo # ---------- # Another group of parallel tests (JSON related) # ---------- -test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson +test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson json_sqljson jsonb_sqljson # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql new file mode 100644 index 0000000000..4f30fa46b9 --- /dev/null +++ b/src/test/regress/sql/json_sqljson.sql @@ -0,0 +1,11 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + +-- JSON_QUERY + +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql new file mode 100644 index 0000000000..b8a6148b7b --- /dev/null +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -0,0 +1,327 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL::jsonb, '$'); + +SELECT JSON_EXISTS(jsonb '[]', '$'); +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + +SELECT JSON_EXISTS(jsonb '1', '$'); +SELECT JSON_EXISTS(jsonb 'null', '$'); +SELECT JSON_EXISTS(jsonb '[]', '$'); + +SELECT JSON_EXISTS(jsonb '1', '$.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_EXISTS(jsonb 'null', '$.a'); +SELECT JSON_EXISTS(jsonb '[]', '$.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); +SELECT JSON_EXISTS(jsonb '{}', '$.a'); +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSON); -- RETURNING FORMAT not allowed + + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL::jsonb, '$'); + +SELECT JSON_VALUE(jsonb 'null', '$'); +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + +SELECT JSON_VALUE(jsonb 'true', '$'); +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + +SELECT JSON_VALUE(jsonb '123', '$'); +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1.23', '$'); +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '"aaa"', '$'); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); + +SELECT JSON_VALUE(jsonb '[]', '$'); +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '{}', '$'); +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1', '$.a'); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed + +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- JSON_QUERY + +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); + +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); + +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); + +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- Test constraints + +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); + +\d test_jsonb_constraints + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; +WHERE constraint_name LIKE 'test_jsonb_constraint%'; + +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +INSERT INTO test_jsonb_constraints VALUES ('[]'); +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); + +DROP TABLE test_jsonb_constraints; + +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 7d60511e9e..336b7faa36 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1254,14 +1254,24 @@ JsonArrayAgg JsonArrayConstructor JsonArrayQueryConstructor JsonBaseObjectInfo +JsonBehavior +JsonBehaviorType +JsonCoercion +JsonCoercionState +JsonCommon JsonConstructorExpr JsonConstructorExprState JsonConstructorType JsonEncoding +JsonExpr +JsonExprOp +JsonExprState JsonFormat JsonFormatType JsonHashEntry JsonIsPredicate +JsonItemCoercions +JsonItemCoercionsState JsonIterateStringValuesAction JsonKeyValue JsonLexContext @@ -1294,6 +1304,10 @@ JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback JsonPathString +JsonPathVarCallback +JsonPathVariable +JsonPathVariableEvalContext +JsonQuotes JsonReturning JsonSemAction JsonSerializeExpr -- 2.35.3
v3-0001-Pass-constructName-to-transformJsonValueExpr.patch (application/octet-stream, 3.0K) download From 5ab5ddc4549e210d5970a20505ef1813c7dcb335 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Fri, 7 Jul 2023 12:08:58 +0900 Subject: [PATCH v3 1/6] Pass constructName to transformJsonValueExpr() Which in turn can be passed to coerce_to_specific_type() so that it can report the name of the specific JSON_* expression being transformed. --- src/backend/parser/parse_expr.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 346fd272b6..9e8a2ac22b 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -3223,6 +3223,7 @@ makeCaseTestExpr(Node *expr) */ static Node * transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, + char *constructName, JsonFormatType default_format) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); @@ -3233,12 +3234,8 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, char typcategory; bool typispreferred; - /* - * Using JSON_VALUE here is slightly bogus: perhaps we need to be passed a - * JsonConstructorType so that we can use one of JSON_OBJECTAGG, etc. - */ if (exprType(expr) == UNKNOWNOID) - expr = coerce_to_specific_type(pstate, expr, TEXTOID, "JSON_VALUE"); + expr = coerce_to_specific_type(pstate, expr, TEXTOID, constructName); rawexpr = expr; exprtype = exprType(expr); @@ -3589,6 +3586,7 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) JsonKeyValue *kv = castNode(JsonKeyValue, lfirst(lc)); Node *key = transformExprRecurse(pstate, (Node *) kv->key); Node *val = transformJsonValueExpr(pstate, kv->value, + "JSON_OBJECT()", JS_FORMAT_DEFAULT); args = lappend(args, key); @@ -3768,7 +3766,8 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) Oid aggtype; key = transformExprRecurse(pstate, (Node *) agg->arg->key); - val = transformJsonValueExpr(pstate, agg->arg->value, JS_FORMAT_DEFAULT); + val = transformJsonValueExpr(pstate, agg->arg->value, "JSON_OBJECTAGG()", + JS_FORMAT_DEFAULT); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3824,7 +3823,8 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) Oid aggfnoid; Oid aggtype; - arg = transformJsonValueExpr(pstate, agg->arg, JS_FORMAT_DEFAULT); + arg = transformJsonValueExpr(pstate, agg->arg, "JSON_ARRAYAGG()", + JS_FORMAT_DEFAULT); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3871,6 +3871,7 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) { JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, jsval, + "JSON_ARRAY()", JS_FORMAT_DEFAULT); args = lappend(args, val); -- 2.35.3
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-10 14:47  Alvaro Herrera <alvherre@alvh.no-ip.org>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Alvaro Herrera @ 2023-07-10 14:47 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: pgsql-hackers

On 2023-Jul-10, Amit Langote wrote:

> > I see that you add json_returning_clause_opt, but we already have
> > json_output_clause_opt.  Shouldn't these two be one and the same?
> > I think the new name is more sensible than the old one, since the
> > governing keyword is RETURNING; I suppose naming it "output" comes from
> > the fact that the standard calls this <JSON output clause>.
> 
> One difference between the two is that json_output_clause_opt allows
> specifying the FORMAT clause in addition to the RETURNING type name,
> while json_returning_clause_op only allows specifying the type name.
> 
> I'm inclined to keep only json_returning_clause_opt as you suggest and
> make parse_expr.c output an error if the FORMAT clause is specified in
> JSON() and JSON_SCALAR(), so turning the current syntax error on
> specifying RETURNING ... FORMAT for these functions into a parsing
> error.   Done that way in the attached updated patch and also updated
> the latter patch that adds JSON_EXISTS() and JSON_VALUE() to have
> similar behavior.

Yeah, that's reasonable.

> > I'm not in love with the fact that JSON and JSONB have pretty much
> > parallel type categorizing functionality. It seems entirely artificial.
> > Maybe this didn't matter when these were contained inside each .c file
> > and nobody else had to deal with that, but I think it's not good to make
> > this an exported concept.  Is it possible to do away with that?  I mean,
> > reduce both to a single categorization enum, and a single categorization
> > API.  Here you have to cast the enum value to int in order to make
> > ExecInitExprRec work, and that seems a bit lame; moreso when the
> > "is_jsonb" is determined separately (cf. ExecEvalJsonConstructor)
> 
> OK, I agree that a unified categorizing API might be better.  I'll
> look at making this better.  Btw, does src/include/common/jsonapi.h
> look like an appropriate place for that?

Hmm, that header is frontend-available, and the type-category appears to
be backend-only, so maybe no.  Perhaps jsonfuncs.h is more apropos?
execExpr.c is already dealing with array internals, so having to deal
with json internals doesn't seem completely out of place.


> > In the 2023 standard, JSON_SCALAR is just
> >
> > <JSON scalar> ::= JSON_SCALAR <left paren> <value expression> <right paren>
> >
> > but we seem to have added a <JSON output format> clause to it.  Should
> > we really?
> 
> Hmm, I am not seeing <JSON output format> in the rule for JSON_SCALAR,

Agh, yeah, I confused myself, sorry.

> Per what I wrote above, the grammar for JSON() and JSON_SCALAR() does
> not allow specifying the FORMAT clause.  Though considering what you
> wrote, the RETURNING clause does appear to be an extension to the
> standard's spec.

Hmm, I see that <JSON output clause> (which is RETURNING plus optional
FORMAT) appears included in JSON_OBJECT, JSON_ARRAY, JSON_QUERY,
JSON_SERIALIZE, JSON_OBJECTAGG, JSON_ARRAYAGG.  It's not necessarily a
bad thing to have it in other places, but we should consider it
carefully.  Do we really want/need it in JSON() and JSON_SCALAR()?

-- 
Álvaro Herrera               48°01'N 7°57'E  —  https://www.EnterpriseDB.com/





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-10 14:51  Alvaro Herrera <alvherre@alvh.no-ip.org>
  parent: Alvaro Herrera <alvherre@alvh.no-ip.org>
  0 siblings, 1 reply; 300+ messages in thread

From: Alvaro Herrera @ 2023-07-10 14:51 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: pgsql-hackers

I forgot to add:

* 0001 looks an obvious improvement.  You could just push it now, to
avoid carrying it forward anymore.  I would just put the constructName
ahead of value expr in the argument list, though.

* 0002: I have no idea what this is (though I probably should).  I would
also push it right away -- if anything, so that we figure out sooner
that it was actually needed in the first place.  Or maybe you just need
the right test cases?

-- 
Álvaro Herrera               48°01'N 7°57'E  —  https://www.EnterpriseDB.com/





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-12 09:41  Amit Langote <amitlangote09@gmail.com>
  parent: Alvaro Herrera <alvherre@alvh.no-ip.org>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-07-12 09:41 UTC (permalink / raw)
  To: Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: pgsql-hackers; Andrew Dunstan <andrew@dunslane.net>

On Mon, Jul 10, 2023 at 11:52 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
> I forgot to add:

Thanks for the review of these.

> * 0001 looks an obvious improvement.  You could just push it now, to
> avoid carrying it forward anymore.  I would just put the constructName
> ahead of value expr in the argument list, though.

Sure, that makes sense.

> * 0002: I have no idea what this is (though I probably should).  I would
> also push it right away -- if anything, so that we figure out sooner
> that it was actually needed in the first place.  Or maybe you just need
> the right test cases?

Hmm, I don't think having or not having CaseTestExpr makes a
difference to the result of evaluating JsonValueExpr.format_expr, so
there are no test cases to prove one way or the other.

After staring at this again for a while, I think I figured out why the
CaseTestExpr might have been put there in the first place.  It seems
to have to do with the fact that JsonValueExpr.raw_expr is currently
evaluated independently of JsonValueExpr.formatted_expr and the
CaseTestExpr propagates the result of the former to the evaluation of
the latter.  Actually, formatted_expr is effectively
formatting_function(<result-of-raw_expr>), so if we put raw_expr
itself into formatted_expr such that it is evaluated as part of
evaluating formatted_expr, then there is no need for the CaseTestExpr
as the propagator for raw_expr's result.

I've expanded the commit message to mention the details.

I'll push these tomorrow.

On Mon, Jul 10, 2023 at 11:47 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
> On 2023-Jul-10, Amit Langote wrote:
> > > I'm not in love with the fact that JSON and JSONB have pretty much
> > > parallel type categorizing functionality. It seems entirely artificial.
> > > Maybe this didn't matter when these were contained inside each .c file
> > > and nobody else had to deal with that, but I think it's not good to make
> > > this an exported concept.  Is it possible to do away with that?  I mean,
> > > reduce both to a single categorization enum, and a single categorization
> > > API.  Here you have to cast the enum value to int in order to make
> > > ExecInitExprRec work, and that seems a bit lame; moreso when the
> > > "is_jsonb" is determined separately (cf. ExecEvalJsonConstructor)
> >
> > OK, I agree that a unified categorizing API might be better.  I'll
> > look at making this better.  Btw, does src/include/common/jsonapi.h
> > look like an appropriate place for that?
>
> Hmm, that header is frontend-available, and the type-category appears to
> be backend-only, so maybe no.  Perhaps jsonfuncs.h is more apropos?
> execExpr.c is already dealing with array internals, so having to deal
> with json internals doesn't seem completely out of place.

OK, attached 0003 does it like that.  Essentially, I decided to only
keep JsonTypeCategory and json_categorize_type(), with some
modifications to accommodate the callers in jsonb.c.

> > > In the 2023 standard, JSON_SCALAR is just
> > >
> > > <JSON scalar> ::= JSON_SCALAR <left paren> <value expression> <right paren>
> > >
> > > but we seem to have added a <JSON output format> clause to it.  Should
> > > we really?
> >
> > Hmm, I am not seeing <JSON output format> in the rule for JSON_SCALAR,
>
> Agh, yeah, I confused myself, sorry.
>
> > Per what I wrote above, the grammar for JSON() and JSON_SCALAR() does
> > not allow specifying the FORMAT clause.  Though considering what you
> > wrote, the RETURNING clause does appear to be an extension to the
> > standard's spec.
>
> Hmm, I see that <JSON output clause> (which is RETURNING plus optional
> FORMAT) appears included in JSON_OBJECT, JSON_ARRAY, JSON_QUERY,
> JSON_SERIALIZE, JSON_OBJECTAGG, JSON_ARRAYAGG.  It's not necessarily a
> bad thing to have it in other places, but we should consider it
> carefully.  Do we really want/need it in JSON() and JSON_SCALAR()?

I thought that removing that support breaks JSON_TABLE() or something
but it doesn't, so maybe we can do without the extension if there's no
particular reason it's there in the first place.  Maybe Andrew (cc'd)
remembers why he decided in [1] to (re-) add the RETURNING clause to
JSON() and JSON_SCALAR()?

Updated patches, with 0003 being a new refactoring patch, are
attached.  Patches 0004~ contain a few updates around JsonValueExpr.
Specifically, I removed the case for T_JsonValueExpr in
transformExprRecurse(), because I realized that JsonValueExpr
expressions never appear embedded in other expressions.  That allowed
me to get rid of some needless refactoring around
transformJsonValueExpr() in the patch that adds JSON_VALUE() etc.

--
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

[1] https://www.postgresql.org/message-id/1d44d832-4ea9-1ec9-81e9-bc6b2bd8cc43%40dunslane.net

Attachments: download all (7 files, .tar.gz)

v4-0003-Unify-JSON-categorize-type-API-and-export-for-ext.patch (application/octet-stream, 27.4K) download From e06f321f2ab29160963db3f77a9b05cf487874af Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Tue, 11 Jul 2023 16:00:42 +0900 Subject: [PATCH v4 3/7] Unify JSON categorize type API and export for external use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This essentially removes the JsonbTypeCategory enum and jsonb_categorize_type() and integrates any jsonb-specific logic that was in jsonb_categorize_type() into json_categorize_type(), now moved to jsonfuncs.c, such that it covers the needs of the callers in both json.c and jsonb.c. The common JsonTypeCategory enum is also exported in jsonfuncs.h. json_categorize_type() has grown a new parameter named is_jsonb for callers in jsonb.c to engage the jsonb-specific behavior of json_categorize_type(). Reviewed-by: Álvaro Herrera <alvherre@alvh.no-ip.org> Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- src/backend/utils/adt/json.c | 136 ++--------------- src/backend/utils/adt/jsonb.c | 245 +++++++----------------------- src/backend/utils/adt/jsonfuncs.c | 111 ++++++++++++++ src/include/utils/jsonfuncs.h | 21 +++ src/tools/pgindent/typedefs.list | 1 - 5 files changed, 199 insertions(+), 315 deletions(-) diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index 49080e5fbf..20f1f87b61 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -19,7 +19,6 @@ #include "funcapi.h" #include "libpq/pqformat.h" #include "miscadmin.h" -#include "parser/parse_coerce.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/date.h" @@ -29,21 +28,6 @@ #include "utils/lsyscache.h" #include "utils/typcache.h" -typedef enum /* type categories for datum_to_json */ -{ - JSONTYPE_NULL, /* null, so we didn't bother to identify */ - JSONTYPE_BOOL, /* boolean (built-in types only) */ - JSONTYPE_NUMERIC, /* numeric (ditto) */ - JSONTYPE_DATE, /* we use special formatting for datetimes */ - JSONTYPE_TIMESTAMP, - JSONTYPE_TIMESTAMPTZ, - JSONTYPE_JSON, /* JSON itself (and JSONB) */ - JSONTYPE_ARRAY, /* array */ - JSONTYPE_COMPOSITE, /* composite */ - JSONTYPE_CAST, /* something with an explicit cast to JSON */ - JSONTYPE_OTHER /* all else */ -} JsonTypeCategory; - /* * Support for fast key uniqueness checking. @@ -107,9 +91,6 @@ static void array_dim_to_json(StringInfo result, int dim, int ndims, int *dims, bool use_line_feeds); static void array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds); -static void json_categorize_type(Oid typoid, - JsonTypeCategory *tcategory, - Oid *outfuncoid); static void datum_to_json(Datum val, bool is_null, StringInfo result, JsonTypeCategory tcategory, Oid outfuncoid, bool key_scalar); @@ -182,106 +163,6 @@ json_recv(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(cstring_to_text_with_len(str, nbytes)); } -/* - * Determine how we want to print values of a given type in datum_to_json. - * - * Given the datatype OID, return its JsonTypeCategory, as well as the type's - * output function OID. If the returned category is JSONTYPE_CAST, we - * return the OID of the type->JSON cast function instead. - */ -static void -json_categorize_type(Oid typoid, - JsonTypeCategory *tcategory, - Oid *outfuncoid) -{ - bool typisvarlena; - - /* Look through any domain */ - typoid = getBaseType(typoid); - - *outfuncoid = InvalidOid; - - /* - * We need to get the output function for everything except date and - * timestamp types, array and composite types, booleans, and non-builtin - * types where there's a cast to json. - */ - - switch (typoid) - { - case BOOLOID: - *tcategory = JSONTYPE_BOOL; - break; - - case INT2OID: - case INT4OID: - case INT8OID: - case FLOAT4OID: - case FLOAT8OID: - case NUMERICOID: - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - *tcategory = JSONTYPE_NUMERIC; - break; - - case DATEOID: - *tcategory = JSONTYPE_DATE; - break; - - case TIMESTAMPOID: - *tcategory = JSONTYPE_TIMESTAMP; - break; - - case TIMESTAMPTZOID: - *tcategory = JSONTYPE_TIMESTAMPTZ; - break; - - case JSONOID: - case JSONBOID: - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - *tcategory = JSONTYPE_JSON; - break; - - default: - /* Check for arrays and composites */ - if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID - || typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID) - *tcategory = JSONTYPE_ARRAY; - else if (type_is_rowtype(typoid)) /* includes RECORDOID */ - *tcategory = JSONTYPE_COMPOSITE; - else - { - /* It's probably the general case ... */ - *tcategory = JSONTYPE_OTHER; - /* but let's look for a cast to json, if it's not built-in */ - if (typoid >= FirstNormalObjectId) - { - Oid castfunc; - CoercionPathType ctype; - - ctype = find_coercion_pathway(JSONOID, typoid, - COERCION_EXPLICIT, - &castfunc); - if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc)) - { - *tcategory = JSONTYPE_CAST; - *outfuncoid = castfunc; - } - else - { - /* non builtin type with no cast */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - } - else - { - /* any other builtin type */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - } - break; - } -} - /* * Turn a Datum into JSON text, appending the string to "result". * @@ -591,7 +472,7 @@ array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds) get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign); - json_categorize_type(element_type, + json_categorize_type(element_type, false, &tcategory, &outfuncoid); deconstruct_array(v, element_type, typlen, typbyval, @@ -665,7 +546,8 @@ composite_to_json(Datum composite, StringInfo result, bool use_line_feeds) outfuncoid = InvalidOid; } else - json_categorize_type(att->atttypid, &tcategory, &outfuncoid); + json_categorize_type(att->atttypid, false, &tcategory, + &outfuncoid); datum_to_json(val, isnull, result, tcategory, outfuncoid, false); } @@ -699,7 +581,7 @@ add_json(Datum val, bool is_null, StringInfo result, outfuncoid = InvalidOid; } else - json_categorize_type(val_type, + json_categorize_type(val_type, false, &tcategory, &outfuncoid); datum_to_json(val, is_null, result, tcategory, outfuncoid, key_scalar); @@ -784,7 +666,7 @@ to_json_is_immutable(Oid typoid) JsonTypeCategory tcategory; Oid outfuncoid; - json_categorize_type(typoid, &tcategory, &outfuncoid); + json_categorize_type(typoid, false, &tcategory, &outfuncoid); switch (tcategory) { @@ -830,7 +712,7 @@ to_json(PG_FUNCTION_ARGS) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - json_categorize_type(val_type, + json_categorize_type(val_type, false, &tcategory, &outfuncoid); result = makeStringInfo(); @@ -880,7 +762,7 @@ json_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null) MemoryContextSwitchTo(oldcontext); appendStringInfoChar(state->str, '['); - json_categorize_type(arg_type, &state->val_category, + json_categorize_type(arg_type, false, &state->val_category, &state->val_output_func); } else @@ -1112,7 +994,7 @@ json_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine data type for argument %d", 1))); - json_categorize_type(arg_type, &state->key_category, + json_categorize_type(arg_type, false, &state->key_category, &state->key_output_func); arg_type = get_fn_expr_argtype(fcinfo->flinfo, 2); @@ -1122,7 +1004,7 @@ json_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine data type for argument %d", 2))); - json_categorize_type(arg_type, &state->val_category, + json_categorize_type(arg_type, false, &state->val_category, &state->val_output_func); appendStringInfoString(state->str, "{ "); diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index cf43c3f2de..fc64f56868 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -19,7 +19,6 @@ #include "funcapi.h" #include "libpq/pqformat.h" #include "miscadmin.h" -#include "parser/parse_coerce.h" #include "utils/builtins.h" #include "utils/date.h" #include "utils/datetime.h" @@ -37,29 +36,12 @@ typedef struct JsonbInState Node *escontext; } JsonbInState; -/* unlike with json categories, we need to treat json and jsonb differently */ -typedef enum /* type categories for datum_to_jsonb */ -{ - JSONBTYPE_NULL, /* null, so we didn't bother to identify */ - JSONBTYPE_BOOL, /* boolean (built-in types only) */ - JSONBTYPE_NUMERIC, /* numeric (ditto) */ - JSONBTYPE_DATE, /* we use special formatting for datetimes */ - JSONBTYPE_TIMESTAMP, /* we use special formatting for timestamp */ - JSONBTYPE_TIMESTAMPTZ, /* ... and timestamptz */ - JSONBTYPE_JSON, /* JSON */ - JSONBTYPE_JSONB, /* JSONB */ - JSONBTYPE_ARRAY, /* array */ - JSONBTYPE_COMPOSITE, /* composite */ - JSONBTYPE_JSONCAST, /* something with an explicit cast to JSON */ - JSONBTYPE_OTHER /* all else */ -} JsonbTypeCategory; - typedef struct JsonbAggState { JsonbInState *res; - JsonbTypeCategory key_category; + JsonTypeCategory key_category; Oid key_output_func; - JsonbTypeCategory val_category; + JsonTypeCategory val_category; Oid val_output_func; } JsonbAggState; @@ -72,19 +54,13 @@ static JsonParseErrorType jsonb_in_array_end(void *pstate); static JsonParseErrorType jsonb_in_object_field_start(void *pstate, char *fname, bool isnull); static void jsonb_put_escaped_value(StringInfo out, JsonbValue *scalarVal); static JsonParseErrorType jsonb_in_scalar(void *pstate, char *token, JsonTokenType tokentype); -static void jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid); static void composite_to_jsonb(Datum composite, JsonbInState *result); static void array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *dims, Datum *vals, bool *nulls, int *valcount, - JsonbTypeCategory tcategory, Oid outfuncoid); + JsonTypeCategory tcategory, Oid outfuncoid); static void array_to_jsonb_internal(Datum array, JsonbInState *result); -static void jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid); static void datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, - JsonbTypeCategory tcategory, Oid outfuncoid, + JsonTypeCategory tcategory, Oid outfuncoid, bool key_scalar); static void add_jsonb(Datum val, bool is_null, JsonbInState *result, Oid val_type, bool key_scalar); @@ -633,112 +609,6 @@ add_indent(StringInfo out, bool indent, int level) } -/* - * Determine how we want to render values of a given type in datum_to_jsonb. - * - * Given the datatype OID, return its JsonbTypeCategory, as well as the type's - * output function OID. If the returned category is JSONBTYPE_JSONCAST, - * we return the OID of the relevant cast function instead. - */ -static void -jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid) -{ - bool typisvarlena; - - /* Look through any domain */ - typoid = getBaseType(typoid); - - *outfuncoid = InvalidOid; - - /* - * We need to get the output function for everything except date and - * timestamp types, booleans, array and composite types, json and jsonb, - * and non-builtin types where there's a cast to json. In this last case - * we return the oid of the cast function instead. - */ - - switch (typoid) - { - case BOOLOID: - *tcategory = JSONBTYPE_BOOL; - break; - - case INT2OID: - case INT4OID: - case INT8OID: - case FLOAT4OID: - case FLOAT8OID: - case NUMERICOID: - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - *tcategory = JSONBTYPE_NUMERIC; - break; - - case DATEOID: - *tcategory = JSONBTYPE_DATE; - break; - - case TIMESTAMPOID: - *tcategory = JSONBTYPE_TIMESTAMP; - break; - - case TIMESTAMPTZOID: - *tcategory = JSONBTYPE_TIMESTAMPTZ; - break; - - case JSONBOID: - *tcategory = JSONBTYPE_JSONB; - break; - - case JSONOID: - *tcategory = JSONBTYPE_JSON; - break; - - default: - /* Check for arrays and composites */ - if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID - || typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID) - *tcategory = JSONBTYPE_ARRAY; - else if (type_is_rowtype(typoid)) /* includes RECORDOID */ - *tcategory = JSONBTYPE_COMPOSITE; - else - { - /* It's probably the general case ... */ - *tcategory = JSONBTYPE_OTHER; - - /* - * but first let's look for a cast to json (note: not to - * jsonb) if it's not built-in. - */ - if (typoid >= FirstNormalObjectId) - { - Oid castfunc; - CoercionPathType ctype; - - ctype = find_coercion_pathway(JSONOID, typoid, - COERCION_EXPLICIT, &castfunc); - if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc)) - { - *tcategory = JSONBTYPE_JSONCAST; - *outfuncoid = castfunc; - } - else - { - /* not a cast type, so just get the usual output func */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - } - else - { - /* any other builtin type */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - break; - } - } -} - /* * Turn a Datum into jsonb, adding it to the result JsonbInState. * @@ -753,7 +623,7 @@ jsonb_categorize_type(Oid typoid, */ static void datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, - JsonbTypeCategory tcategory, Oid outfuncoid, + JsonTypeCategory tcategory, Oid outfuncoid, bool key_scalar) { char *outputstr; @@ -770,11 +640,11 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, jb.type = jbvNull; } else if (key_scalar && - (tcategory == JSONBTYPE_ARRAY || - tcategory == JSONBTYPE_COMPOSITE || - tcategory == JSONBTYPE_JSON || - tcategory == JSONBTYPE_JSONB || - tcategory == JSONBTYPE_JSONCAST)) + (tcategory == JSONTYPE_ARRAY || + tcategory == JSONTYPE_COMPOSITE || + tcategory == JSONTYPE_JSON || + tcategory == JSONTYPE_JSONB || + tcategory == JSONTYPE_JSON)) { ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -782,18 +652,18 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, } else { - if (tcategory == JSONBTYPE_JSONCAST) + if (tcategory == JSONTYPE_CAST) val = OidFunctionCall1(outfuncoid, val); switch (tcategory) { - case JSONBTYPE_ARRAY: + case JSONTYPE_ARRAY: array_to_jsonb_internal(val, result); break; - case JSONBTYPE_COMPOSITE: + case JSONTYPE_COMPOSITE: composite_to_jsonb(val, result); break; - case JSONBTYPE_BOOL: + case JSONTYPE_BOOL: if (key_scalar) { outputstr = DatumGetBool(val) ? "true" : "false"; @@ -807,7 +677,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, jb.val.boolean = DatumGetBool(val); } break; - case JSONBTYPE_NUMERIC: + case JSONTYPE_NUMERIC: outputstr = OidOutputFunctionCall(outfuncoid, val); if (key_scalar) { @@ -845,26 +715,26 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, } } break; - case JSONBTYPE_DATE: + case JSONTYPE_DATE: jb.type = jbvString; jb.val.string.val = JsonEncodeDateTime(NULL, val, DATEOID, NULL); jb.val.string.len = strlen(jb.val.string.val); break; - case JSONBTYPE_TIMESTAMP: + case JSONTYPE_TIMESTAMP: jb.type = jbvString; jb.val.string.val = JsonEncodeDateTime(NULL, val, TIMESTAMPOID, NULL); jb.val.string.len = strlen(jb.val.string.val); break; - case JSONBTYPE_TIMESTAMPTZ: + case JSONTYPE_TIMESTAMPTZ: jb.type = jbvString; jb.val.string.val = JsonEncodeDateTime(NULL, val, TIMESTAMPTZOID, NULL); jb.val.string.len = strlen(jb.val.string.val); break; - case JSONBTYPE_JSONCAST: - case JSONBTYPE_JSON: + case JSONTYPE_CAST: + case JSONTYPE_JSON: { /* parse the json right into the existing result object */ JsonLexContext *lex; @@ -887,7 +757,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, pg_parse_json_or_ereport(lex, &sem); } break; - case JSONBTYPE_JSONB: + case JSONTYPE_JSONB: { Jsonb *jsonb = DatumGetJsonbP(val); JsonbIterator *it; @@ -931,7 +801,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, /* Now insert jb into result, unless we did it recursively */ if (!is_null && !scalar_jsonb && - tcategory >= JSONBTYPE_JSON && tcategory <= JSONBTYPE_JSONCAST) + tcategory >= JSONTYPE_JSON && tcategory <= JSONTYPE_CAST) { /* work has been done recursively */ return; @@ -976,7 +846,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, */ static void array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *dims, Datum *vals, - bool *nulls, int *valcount, JsonbTypeCategory tcategory, + bool *nulls, int *valcount, JsonTypeCategory tcategory, Oid outfuncoid) { int i; @@ -1020,7 +890,7 @@ array_to_jsonb_internal(Datum array, JsonbInState *result) int16 typlen; bool typbyval; char typalign; - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; ndim = ARR_NDIM(v); @@ -1037,8 +907,8 @@ array_to_jsonb_internal(Datum array, JsonbInState *result) get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign); - jsonb_categorize_type(element_type, - &tcategory, &outfuncoid); + json_categorize_type(element_type, true, + &tcategory, &outfuncoid); deconstruct_array(v, element_type, typlen, typbyval, typalign, &elements, &nulls, @@ -1084,7 +954,7 @@ composite_to_jsonb(Datum composite, JsonbInState *result) Datum val; bool isnull; char *attname; - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; JsonbValue v; Form_pg_attribute att = TupleDescAttr(tupdesc, i); @@ -1105,11 +975,12 @@ composite_to_jsonb(Datum composite, JsonbInState *result) if (isnull) { - tcategory = JSONBTYPE_NULL; + tcategory = JSONTYPE_NULL; outfuncoid = InvalidOid; } else - jsonb_categorize_type(att->atttypid, &tcategory, &outfuncoid); + json_categorize_type(att->atttypid, true, &tcategory, + &outfuncoid); datum_to_jsonb(val, isnull, result, tcategory, outfuncoid, false); } @@ -1122,7 +993,7 @@ composite_to_jsonb(Datum composite, JsonbInState *result) * Append JSON text for "val" to "result". * * This is just a thin wrapper around datum_to_jsonb. If the same type will be - * printed many times, avoid using this; better to do the jsonb_categorize_type + * printed many times, avoid using this; better to do the json_categorize_type * lookups only once. */ @@ -1130,7 +1001,7 @@ static void add_jsonb(Datum val, bool is_null, JsonbInState *result, Oid val_type, bool key_scalar) { - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; if (val_type == InvalidOid) @@ -1140,12 +1011,12 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, if (is_null) { - tcategory = JSONBTYPE_NULL; + tcategory = JSONTYPE_NULL; outfuncoid = InvalidOid; } else - jsonb_categorize_type(val_type, - &tcategory, &outfuncoid); + json_categorize_type(val_type, true, + &tcategory, &outfuncoid); datum_to_jsonb(val, is_null, result, tcategory, outfuncoid, key_scalar); } @@ -1160,33 +1031,33 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, bool to_jsonb_is_immutable(Oid typoid) { - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; - jsonb_categorize_type(typoid, &tcategory, &outfuncoid); + json_categorize_type(typoid, true, &tcategory, &outfuncoid); switch (tcategory) { - case JSONBTYPE_NULL: - case JSONBTYPE_BOOL: - case JSONBTYPE_JSON: - case JSONBTYPE_JSONB: + case JSONTYPE_NULL: + case JSONTYPE_BOOL: + case JSONTYPE_JSON: + case JSONTYPE_JSONB: return true; - case JSONBTYPE_DATE: - case JSONBTYPE_TIMESTAMP: - case JSONBTYPE_TIMESTAMPTZ: + case JSONTYPE_DATE: + case JSONTYPE_TIMESTAMP: + case JSONTYPE_TIMESTAMPTZ: return false; - case JSONBTYPE_ARRAY: + case JSONTYPE_ARRAY: return false; /* TODO recurse into elements */ - case JSONBTYPE_COMPOSITE: + case JSONTYPE_COMPOSITE: return false; /* TODO recurse into fields */ - case JSONBTYPE_NUMERIC: - case JSONBTYPE_JSONCAST: - case JSONBTYPE_OTHER: + case JSONTYPE_NUMERIC: + case JSONTYPE_CAST: + case JSONTYPE_OTHER: return func_volatile(outfuncoid) == PROVOLATILE_IMMUTABLE; } @@ -1202,7 +1073,7 @@ to_jsonb(PG_FUNCTION_ARGS) Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); JsonbInState result; - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; if (val_type == InvalidOid) @@ -1210,8 +1081,8 @@ to_jsonb(PG_FUNCTION_ARGS) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - jsonb_categorize_type(val_type, - &tcategory, &outfuncoid); + json_categorize_type(val_type, true, + &tcategory, &outfuncoid); memset(&result, 0, sizeof(JsonbInState)); @@ -1636,8 +1507,8 @@ jsonb_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null) WJB_BEGIN_ARRAY, NULL); MemoryContextSwitchTo(oldcontext); - jsonb_categorize_type(arg_type, &state->val_category, - &state->val_output_func); + json_categorize_type(arg_type, true, &state->val_category, + &state->val_output_func); } else { @@ -1816,8 +1687,8 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - jsonb_categorize_type(arg_type, &state->key_category, - &state->key_output_func); + json_categorize_type(arg_type, true, &state->key_category, + &state->key_output_func); arg_type = get_fn_expr_argtype(fcinfo->flinfo, 2); @@ -1826,8 +1697,8 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - jsonb_categorize_type(arg_type, &state->val_category, - &state->val_output_func); + json_categorize_type(arg_type, true, &state->val_category, + &state->val_output_func); } else { diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index 70cb922e6b..612bbf06a3 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -26,6 +26,7 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" #include "nodes/miscnodes.h" +#include "parser/parse_coerce.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -5685,3 +5686,113 @@ json_get_first_token(text *json, bool throw_error) return JSON_TOKEN_INVALID; /* invalid json */ } + +/* + * Determine how we want to print values of a given type in datum_to_json(b). + * + * Given the datatype OID, return its JsonTypeCategory, as well as the type's + * output function OID. If the returned category is JSONTYPE_CAST or + * JSOBTYPE_CASTJSON, we return the OID of the type->JSON cast function + * instead. + */ +void +json_categorize_type(Oid typoid, bool is_jsonb, + JsonTypeCategory *tcategory, Oid *outfuncoid) +{ + bool typisvarlena; + + /* Look through any domain */ + typoid = getBaseType(typoid); + + *outfuncoid = InvalidOid; + + /* + * We need to get the output function for everything except date and + * timestamp types, booleans, array and composite types, json and jsonb, + * and non-builtin types where there's a cast to json. In this last case + * we return the oid of the cast function instead. + */ + + switch (typoid) + { + case BOOLOID: + *tcategory = JSONTYPE_BOOL; + break; + + case INT2OID: + case INT4OID: + case INT8OID: + case FLOAT4OID: + case FLOAT8OID: + case NUMERICOID: + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + *tcategory = JSONTYPE_NUMERIC; + break; + + case DATEOID: + *tcategory = JSONTYPE_DATE; + break; + + case TIMESTAMPOID: + *tcategory = JSONTYPE_TIMESTAMP; + break; + + case TIMESTAMPTZOID: + *tcategory = JSONTYPE_TIMESTAMPTZ; + break; + + case JSONOID: + if (!is_jsonb) + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + *tcategory = JSONTYPE_JSON; + break; + + case JSONBOID: + if (!is_jsonb) + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + *tcategory = is_jsonb ? JSONTYPE_JSONB : JSONTYPE_JSON; + break; + + default: + /* Check for arrays and composites */ + if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID + || typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID) + *tcategory = JSONTYPE_ARRAY; + else if (type_is_rowtype(typoid)) /* includes RECORDOID */ + *tcategory = JSONTYPE_COMPOSITE; + else + { + /* + * It's probably the general case. But let's look for a cast + * to json (note: not to jsonb even if is_jsonb is true), if + * it's not built-in. + */ + *tcategory = JSONTYPE_OTHER; + if (typoid >= FirstNormalObjectId) + { + Oid castfunc; + CoercionPathType ctype; + + ctype = find_coercion_pathway(JSONOID, typoid, + COERCION_EXPLICIT, + &castfunc); + if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc)) + { + *outfuncoid = castfunc; + *tcategory = JSONTYPE_CAST; + } + else + { + /* non builtin type with no cast */ + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + } + } + else + { + /* any other builtin type */ + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + } + } + break; + } +} diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index a85203d4a4..6e9ea2df93 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -63,4 +63,25 @@ extern Jsonb *transform_jsonb_string_values(Jsonb *jsonb, void *action_state, extern text *transform_json_string_values(text *json, void *action_state, JsonTransformStringValuesAction transform_action); +/* Type categories for datum_to_json[b] and friends. */ +typedef enum +{ + JSONTYPE_NULL, /* null, so we didn't bother to identify */ + JSONTYPE_BOOL, /* boolean (built-in types only) */ + JSONTYPE_NUMERIC, /* numeric (ditto) */ + JSONTYPE_DATE, /* we use special formatting for datetimes */ + JSONTYPE_TIMESTAMP, + JSONTYPE_TIMESTAMPTZ, + JSONTYPE_JSON, /* JSON and JSONB */ + JSONTYPE_JSONB, /* JSONB (for datum_to_jsonb) */ + JSONTYPE_ARRAY, /* array */ + JSONTYPE_COMPOSITE, /* composite */ + JSONTYPE_CAST, /* something with an explicit cast to JSON */ + JSONTYPE_JSONCAST, + JSONTYPE_OTHER, /* all else */ +} JsonTypeCategory; + +void json_categorize_type(Oid typoid, bool is_jsonb, + JsonTypeCategory *tcategory, Oid *outfuncoid); + #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e941fb6c82..b10590a252 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1318,7 +1318,6 @@ JsonbIteratorToken JsonbPair JsonbParseState JsonbSubWorkspace -JsonbTypeCategory JsonbValue JumbleState JunkFilter -- 2.35.3
v4-0004-SQL-JSON-functions.patch (application/octet-stream, 53.5K) download From a7f20a132a5868c7dc4a75e610c11192c09f01d4 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:12:39 +0900 Subject: [PATCH v4 4/7] SQL JSON functions This Patch introduces three SQL standard JSON functions: JSON() JSON_SCALAR() JSON_SERIALIZE() JSON() produces json values from text, bytea, json or jsonb values, and has facilitites for handling duplicate keys. JSON_SCALAR() produces a json value from any scalar sql value, including json and jsonb. JSON_SERIALIZE() produces text or bytea from input which containis or represents json or jsonb; For the most part these functions don't add any significant new capabilities, but they will be of use to users wanting standard compliant JSON handling. Nikita Glukhov Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby. Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org --- doc/src/sgml/func.sgml | 71 +++++ src/backend/executor/execExpr.c | 30 ++ src/backend/executor/execExprInterp.c | 43 ++- src/backend/nodes/nodeFuncs.c | 30 ++ src/backend/parser/gram.y | 69 ++++- src/backend/parser/parse_expr.c | 234 +++++++++++++++- src/backend/parser/parse_target.c | 9 + src/backend/utils/adt/format_type.c | 4 + src/backend/utils/adt/json.c | 21 +- src/backend/utils/adt/jsonb.c | 48 +++- src/backend/utils/adt/ruleutils.c | 14 +- src/include/nodes/parsenodes.h | 48 ++++ src/include/nodes/primnodes.h | 5 +- src/include/parser/kwlist.h | 4 +- src/include/utils/jsonb.h | 2 +- src/include/utils/jsonfuncs.h | 4 + src/test/regress/expected/sqljson.out | 384 ++++++++++++++++++++++++++ src/test/regress/sql/sqljson.sql | 95 +++++++ src/tools/pgindent/typedefs.list | 1 + 19 files changed, 1067 insertions(+), 49 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 0b62e0c828..9cece06c18 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16001,6 +16001,77 @@ table2-mapping <returnvalue>{"a": "1", "b": "2"}</returnvalue> </para></entry> </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json constructor</primary></indexterm> + <function>json</function> ( + <replaceable>expression</replaceable> + <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional></optional> + <optional> { <literal>WITH</literal> | <literal>WITHOUT</literal> } <literal>UNIQUE</literal> <optional> <literal>KEYS</literal> </optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>json_data_type</replaceable> </optional>) + </para> + <para> + The <replaceable>expression</replaceable> can be any text type or a + <type>bytea</type> in UTF8 encoding. If the + <replaceable>expression</replaceable> is NULL, an + <acronym>SQL</acronym> null value is returned. + If <literal>WITH UNIQUE</literal> is specified, the + <replaceable>expression</replaceable> must not contain any duplicate + object keys. + </para> + <para> + <literal>json('{"a":123, "b":[true,"foo"], "a":"bar"}')</literal> + <returnvalue>{"a":123, "b":[true,"foo"], "a":"bar"}</returnvalue> + </para> + <para> + <literal>json('{"a":123,"b":[true,"foo"],"a":"bar"}' returning jsonb)</literal> + <returnvalue>{"a": "bar", "b": [true, "foo"]}</returnvalue> + </para> + </entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json_scalar</primary></indexterm> + <function>json_scalar</function> (<replaceable>expression</replaceable>) + <optional> <literal>RETURNING</literal> <replaceable>json_data_type</replaceable> </optional>) + </para> + <para> + Returns a JSON scalar value representing + <replaceable>expression</replaceable>. + If the input is NULL, an SQL NULL is returned. If the input is a number + or a boolean value, a corresponding JSON number or boolean value is + returned. For any other value a JSON string is returned. + </para> + <para> + <literal>json_scalar(123.45)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_scalar(CURRENT_TIMESTAMP)</literal> + <returnvalue>"2022-05-10T10:51:04.62128-04:00"</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <function>json_serialize</function> ( + <replaceable>expression</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional>) + </para> + <para> + Transforms an SQL/JSON value into a character or binary string. The + <replaceable>expression</replaceable> can be of any JSON type, any + character string type, or <type>bytea</type> in UTF8 encoding. + The returned type can be any character string type or + <type>bytea</type>. The default is <type>text</type>. + </para> + <para> + <literal>json_serialize('{ "a" : 1 } ' RETURNING bytea)</literal> + <returnvalue>\x7b20226122203a2031207d20</returnvalue> + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index bf3a08c5f0..6ca4098bef 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -48,6 +48,7 @@ #include "utils/array.h" #include "utils/builtins.h" #include "utils/datum.h" +#include "utils/jsonfuncs.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -2311,6 +2312,12 @@ ExecInitExprRec(Expr *node, ExprState *state, { ExecInitExprRec(ctor->func, state, resv, resnull); } + else if ((ctor->type == JSCTOR_JSON_PARSE && !ctor->unique) || + ctor->type == JSCTOR_JSON_SERIALIZE) + { + /* Use the value of the first argument as a result */ + ExecInitExprRec(linitial(args), state, resv, resnull); + } else { JsonConstructorExprState *jcstate; @@ -2349,6 +2356,29 @@ ExecInitExprRec(Expr *node, ExprState *state, argno++; } + /* prepare type cache for datum_to_json[b]() */ + if (ctor->type == JSCTOR_JSON_SCALAR) + { + bool is_jsonb = + ctor->returning->format->format_type == JS_FORMAT_JSONB; + + jcstate->arg_type_cache = + palloc(sizeof(*jcstate->arg_type_cache) * nargs); + + for (int i = 0; i < nargs; i++) + { + JsonTypeCategory category; + Oid outfuncid; + Oid typid = jcstate->arg_types[i]; + + json_categorize_type(typid, is_jsonb, + &category, &outfuncid); + + jcstate->arg_type_cache[i].outfuncid = outfuncid; + jcstate->arg_type_cache[i].category = (int) category; + } + } + ExprEvalPushStep(state, &scratch); } diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 851946a927..76e59691e5 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -3992,7 +3992,7 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_values, jcstate->arg_nulls, jcstate->arg_types, - jcstate->constructor->absent_on_null); + ctor->absent_on_null); else if (ctor->type == JSCTOR_JSON_OBJECT) res = (is_jsonb ? jsonb_build_object_worker : @@ -4002,6 +4002,47 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_types, jcstate->constructor->absent_on_null, jcstate->constructor->unique); + else if (ctor->type == JSCTOR_JSON_SCALAR) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + Oid outfuncid = jcstate->arg_type_cache[0].outfuncid; + JsonTypeCategory category = (JsonTypeCategory) + jcstate->arg_type_cache[0].category; + + if (is_jsonb) + res = to_jsonb_worker(value, category, outfuncid); + else + res = to_json_worker(value, category, outfuncid); + } + } + else if (ctor->type == JSCTOR_JSON_PARSE) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + text *js = DatumGetTextP(value); + + if (is_jsonb) + res = jsonb_from_text(js, true); + else + { + (void) json_validate(js, true, true); + res = value; + } + } + } else elog(ERROR, "invalid JsonConstructorExpr type %d", ctor->type); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index c41e6bb984..dda964bd19 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -3901,6 +3901,36 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonParseExpr: + { + JsonParseExpr *jpe = (JsonParseExpr *) node; + + if (WALK(jpe->expr)) + return true; + if (WALK(jpe->output)) + return true; + } + break; + case T_JsonScalarExpr: + { + JsonScalarExpr *jse = (JsonScalarExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; + case T_JsonSerializeExpr: + { + JsonSerializeExpr *jse = (JsonSerializeExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; case T_JsonConstructorExpr: { JsonConstructorExpr *ctor = (JsonConstructorExpr *) node; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index edb6c00ece..341b002dc7 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -566,7 +566,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <list> copy_options %type <typnam> Typename SimpleTypename ConstTypename - GenericType Numeric opt_float + GenericType Numeric opt_float JsonType Character ConstCharacter CharacterWithLength CharacterWithoutLength ConstDatetime ConstInterval @@ -647,7 +647,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> json_format_clause_opt json_value_expr - json_output_clause_opt + json_returning_clause_opt json_name_and_value json_aggregate_func %type <list> json_name_and_value_list @@ -659,7 +659,6 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt - /* * Non-keyword token types. These are hard-wired into the "flex" lexer. * They must be listed first so that their numeric codes do not depend on @@ -723,6 +722,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG + JSON_SCALAR JSON_SERIALIZE KEY KEYS @@ -13981,6 +13981,7 @@ SimpleTypename: $$->typmods = list_make2(makeIntConst(INTERVAL_FULL_RANGE, -1), makeIntConst($3, @3)); } + | JsonType { $$ = $1; } ; /* We have a separate ConstTypename to allow defaulting fixed-length @@ -13999,6 +14000,7 @@ ConstTypename: | ConstBit { $$ = $1; } | ConstCharacter { $$ = $1; } | ConstDatetime { $$ = $1; } + | JsonType { $$ = $1; } ; /* @@ -14367,6 +14369,13 @@ interval_second: } ; +JsonType: + JSON + { + $$ = SystemTypeName("json"); + $$->location = @1; + } + ; /***************************************************************************** * @@ -15561,7 +15570,7 @@ func_expr_common_subexpr: | JSON_OBJECT '(' json_name_and_value_list json_object_constructor_null_clause_opt json_key_uniqueness_constraint_opt - json_output_clause_opt ')' + json_returning_clause_opt ')' { JsonObjectConstructor *n = makeNode(JsonObjectConstructor); @@ -15572,7 +15581,7 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } - | JSON_OBJECT '(' json_output_clause_opt ')' + | JSON_OBJECT '(' json_returning_clause_opt ')' { JsonObjectConstructor *n = makeNode(JsonObjectConstructor); @@ -15586,7 +15595,7 @@ func_expr_common_subexpr: | JSON_ARRAY '(' json_value_expr_list json_array_constructor_null_clause_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayConstructor *n = makeNode(JsonArrayConstructor); @@ -15601,7 +15610,7 @@ func_expr_common_subexpr: select_no_parens json_format_clause_opt /* json_array_constructor_null_clause_opt */ - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayQueryConstructor *n = makeNode(JsonArrayQueryConstructor); @@ -15614,7 +15623,7 @@ func_expr_common_subexpr: $$ = (Node *) n; } | JSON_ARRAY '(' - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayConstructor *n = makeNode(JsonArrayConstructor); @@ -15625,7 +15634,37 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } - ; + | JSON '(' json_value_expr json_key_uniqueness_constraint_opt + json_returning_clause_opt ')' + { + JsonParseExpr *n = makeNode(JsonParseExpr); + + n->expr = (JsonValueExpr *) $3; + n->unique_keys = $4; + n->output = (JsonOutput *) $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SCALAR '(' a_expr json_returning_clause_opt ')' + { + JsonScalarExpr *n = makeNode(JsonScalarExpr); + + n->expr = (Expr *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SERIALIZE '(' json_value_expr json_returning_clause_opt ')' + { + JsonSerializeExpr *n = makeNode(JsonSerializeExpr); + + n->expr = (JsonValueExpr *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + ; + /* * SQL/XML support @@ -16373,7 +16412,7 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; -json_output_clause_opt: +json_returning_clause_opt: RETURNING Typename json_format_clause_opt { JsonOutput *n = makeNode(JsonOutput); @@ -16446,7 +16485,7 @@ json_aggregate_func: json_name_and_value json_object_constructor_null_clause_opt json_key_uniqueness_constraint_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonObjectAgg *n = makeNode(JsonObjectAgg); @@ -16464,7 +16503,7 @@ json_aggregate_func: json_value_expr json_array_aggregate_order_by_clause_opt json_array_constructor_null_clause_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayAgg *n = makeNode(JsonArrayAgg); @@ -17064,7 +17103,6 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION - | JSON | KEY | KEYS | LABEL @@ -17279,10 +17317,13 @@ col_name_keyword: | INT_P | INTEGER | INTERVAL + | JSON | JSON_ARRAY | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | LEAST | NATIONAL | NCHAR @@ -17643,6 +17684,8 @@ bare_label_keyword: | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | KEY | KEYS | LABEL diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 20cbb0a8e7..8c41c1f1c5 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -86,6 +86,10 @@ static Node *transformJsonArrayQueryConstructor(ParseState *pstate, static Node *transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg); static Node *transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg); static Node *transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred); +static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); +static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); +static Node *transformJsonSerializeExpr(ParseState *pstate, + JsonSerializeExpr * expr); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -337,6 +341,18 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonIsPredicate(pstate, (JsonIsPredicate *) expr); break; + case T_JsonParseExpr: + result = transformJsonParseExpr(pstate, (JsonParseExpr *) expr); + break; + + case T_JsonScalarExpr: + result = transformJsonScalarExpr(pstate, (JsonScalarExpr *) expr); + break; + + case T_JsonSerializeExpr: + result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3223,7 +3239,8 @@ makeCaseTestExpr(Node *expr) */ static Node * transformJsonValueExpr(ParseState *pstate, char *constructName, - JsonValueExpr *ve, JsonFormatType default_format) + JsonValueExpr *ve, JsonFormatType default_format, + Oid targettype) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3265,12 +3282,14 @@ transformJsonValueExpr(ParseState *pstate, char *constructName, else format = default_format; - if (format != JS_FORMAT_DEFAULT) + if (format != JS_FORMAT_DEFAULT || + (OidIsValid(targettype) && exprtype != targettype)) { - Oid targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; Node *coerced; + bool cast_is_needed = OidIsValid(targettype); - if (exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) + if (!cast_is_needed && + exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), errmsg(ve->format->format_type == JS_FORMAT_DEFAULT ? @@ -3286,6 +3305,9 @@ transformJsonValueExpr(ParseState *pstate, char *constructName, exprtype = TEXTOID; } + if (!OidIsValid(targettype)) + targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; + /* Try to coerce to the target type. */ coerced = coerce_to_target_type(pstate, expr, exprtype, targettype, -1, @@ -3296,11 +3318,20 @@ transformJsonValueExpr(ParseState *pstate, char *constructName, if (!coerced) { /* If coercion failed, use to_json()/to_jsonb() functions. */ - Oid fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; - FuncExpr *fexpr = makeFuncExpr(fnoid, targettype, - list_make1(expr), - InvalidOid, InvalidOid, - COERCE_EXPLICIT_CALL); + FuncExpr *fexpr; + Oid fnoid; + + if (cast_is_needed) /* only CAST is allowed */ + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(exprtype), + format_type_be(targettype)), + parser_errposition(pstate, location))); + + fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; + fexpr = makeFuncExpr(fnoid, targettype, list_make1(expr), + InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL); fexpr->location = location; @@ -3583,7 +3614,8 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) Node *key = transformExprRecurse(pstate, (Node *) kv->key); Node *val = transformJsonValueExpr(pstate, "JSON_OBJECT()", kv->value, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, key); args = lappend(args, val); @@ -3767,9 +3799,8 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) Oid aggtype; key = transformExprRecurse(pstate, (Node *) agg->arg->key); - val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", - agg->arg->value, - JS_FORMAT_DEFAULT); + val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", agg->arg->value, + JS_FORMAT_DEFAULT, InvalidOid); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3827,7 +3858,7 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", agg->arg, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, InvalidOid); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3875,7 +3906,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, "JSON_ARRAY()", jsval, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, val); } @@ -3958,3 +3990,175 @@ transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred) return makeJsonIsPredicate(expr, NULL, pred->item_type, pred->unique_keys, pred->location); } + +/* + * Transform the output clause of a JSON_*() expression if there is one and + * create one if not. + */ +static JsonReturning * +transformJsonReturning(ParseState *pstate, JsonOutput *output, const char *fname) +{ + JsonReturning *returning; + + if (output) + { + returning = transformJsonOutput(pstate, output, false); + + Assert(OidIsValid(returning->typid)); + + if (returning->typid != JSONOID && returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), fname), + parser_errposition(pstate, output->typeName->location))); + } + else + { + /* Output type is JSON by default. */ + Oid targettype = JSONOID; + JsonFormatType format = JS_FORMAT_JSON; + + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(format, JS_ENC_DEFAULT, -1); + returning->typid = targettype; + returning->typmod = -1; + } + + return returning; +} + +/* + * Transform a JSON() expression. + */ +static Node * +transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) +{ + JsonOutput *output = jsexpr->output; + JsonReturning *returning; + Node *arg; + + /* Disallow FORMAT specification in the RETURNING clause. */ + if (output) + { + JsonFormat *format = output->returning->format; + + if(format->format_type != JS_FORMAT_DEFAULT || + format->encoding != JS_ENC_DEFAULT) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot specify FORMAT in RETURNING clause of JSON()"), + parser_errposition(pstate, format->location))); + } + + returning = transformJsonReturning(pstate, output, "JSON()"); + + if (jsexpr->unique_keys) + { + /* + * Coerce string argument to text and then to json[b] in the executor + * node with key uniqueness check. + */ + JsonValueExpr *jve = jsexpr->expr; + Oid arg_type; + + arg = transformJsonParseArg(pstate, (Node *) jve->raw_expr, jve->format, + &arg_type); + + if (arg_type != TEXTOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use non-string types with WITH UNIQUE KEYS clause"), + parser_errposition(pstate, jsexpr->location))); + } + else + { + /* + * Coerce argument to target type using CAST for compatibility with PG + * function-like CASTs. + */ + arg = transformJsonValueExpr(pstate, "JSON()", jsexpr->expr, + JS_FORMAT_JSON, returning->typid); + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, + returning, jsexpr->unique_keys, false, + jsexpr->location); +} + +/* + * Transform a JSON_SCALAR() expression. + */ +static Node * +transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *jsexpr) +{ + Node *arg = transformExprRecurse(pstate, (Node *) jsexpr->expr); + JsonOutput *output = jsexpr->output; + JsonReturning *returning; + + /* Disallow FORMAT specification in the RETURNING clause. */ + if (output) + { + JsonFormat *format = output->returning->format; + + if(format->format_type != JS_FORMAT_DEFAULT || + format->encoding != JS_ENC_DEFAULT) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot specify FORMAT in RETURNING clause of JSON_SCALAR()"), + parser_errposition(pstate, format->location))); + } + + returning = transformJsonReturning(pstate, output, "JSON_SCALAR()"); + + if (exprType(arg) == UNKNOWNOID) + arg = coerce_to_specific_type(pstate, arg, TEXTOID, "JSON_SCALAR"); + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SCALAR, list_make1(arg), NULL, + returning, false, false, jsexpr->location); +} + +/* + * Transform a JSON_SERIALIZE() expression. + */ +static Node * +transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) +{ + Node *arg = transformJsonValueExpr(pstate, "JSON_SERIALIZE()", + expr->expr, + JS_FORMAT_JSON, + InvalidOid); + JsonReturning *returning; + + if (expr->output) + { + returning = transformJsonOutput(pstate, expr->output, true); + + if (returning->typid != BYTEAOID) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(returning->typid, &typcategory, + &typispreferred); + if (typcategory != TYPCATEGORY_STRING) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), + "JSON_SERIALIZE()"), + errhint("Try returning a string type or bytea."))); + } + } + else + { + /* RETURNING TEXT FORMAT JSON is by default */ + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(JS_FORMAT_JSON, JS_ENC_DEFAULT, -1); + returning->typid = TEXTOID; + returning->typmod = -1; + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), + NULL, returning, false, false, expr->location); +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4cca97ff9c..520d4f2a23 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1953,6 +1953,15 @@ FigureColnameInternal(Node *node, char **name) /* make XMLSERIALIZE act like a regular function */ *name = "xmlserialize"; return 2; + case T_JsonParseExpr: + *name = "json"; + return 2; + case T_JsonScalarExpr: + *name = "json_scalar"; + return 2; + case T_JsonSerializeExpr: + *name = "json_serialize"; + return 2; case T_JsonObjectConstructor: /* make JSON_OBJECT act like a regular function */ *name = "json_object"; diff --git a/src/backend/utils/adt/format_type.c b/src/backend/utils/adt/format_type.c index 12402a0637..36c45a3978 100644 --- a/src/backend/utils/adt/format_type.c +++ b/src/backend/utils/adt/format_type.c @@ -294,6 +294,10 @@ format_type_extended(Oid type_oid, int32 typemod, bits16 flags) else buf = pstrdup("character varying"); break; + + case JSONOID: + buf = pstrdup("json"); + break; } if (buf == NULL) diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index 20f1f87b61..5df0fccdd4 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -653,6 +653,20 @@ row_to_json_pretty(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); } +/* + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. + */ +Datum +to_json_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid) +{ + StringInfo result = makeStringInfo(); + + datum_to_json(val, false, result, tcategory, outfuncoid, false); + + return PointerGetDatum(cstring_to_text_with_len(result->data, result->len)); +} + /* * Is the given type immutable when coming out of a JSON context? * @@ -703,7 +717,6 @@ to_json(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - StringInfo result; JsonTypeCategory tcategory; Oid outfuncoid; @@ -715,11 +728,7 @@ to_json(PG_FUNCTION_ARGS) json_categorize_type(val_type, false, &tcategory, &outfuncoid); - result = makeStringInfo(); - - datum_to_json(val, false, result, tcategory, outfuncoid, false); - - PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); + PG_RETURN_DATUM(to_json_worker(val, tcategory, outfuncoid)); } /* diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index fc64f56868..06ba409e64 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -33,6 +33,7 @@ typedef struct JsonbInState { JsonbParseState *parseState; JsonbValue *res; + bool unique_keys; Node *escontext; } JsonbInState; @@ -45,7 +46,8 @@ typedef struct JsonbAggState Oid val_output_func; } JsonbAggState; -static inline Datum jsonb_from_cstring(char *json, int len, Node *escontext); +static inline Datum jsonb_from_cstring(char *json, int len, bool unique_keys, + Node *escontext); static bool checkStringLen(size_t len, Node *escontext); static JsonParseErrorType jsonb_in_object_start(void *pstate); static JsonParseErrorType jsonb_in_object_end(void *pstate); @@ -76,7 +78,7 @@ jsonb_in(PG_FUNCTION_ARGS) { char *json = PG_GETARG_CSTRING(0); - return jsonb_from_cstring(json, strlen(json), fcinfo->context); + return jsonb_from_cstring(json, strlen(json), false, fcinfo->context); } /* @@ -100,7 +102,7 @@ jsonb_recv(PG_FUNCTION_ARGS) else elog(ERROR, "unsupported jsonb version number %d", version); - return jsonb_from_cstring(str, nbytes, NULL); + return jsonb_from_cstring(str, nbytes, false, NULL); } /* @@ -141,6 +143,18 @@ jsonb_send(PG_FUNCTION_ARGS) PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); } +/* + * Note: exported for use in SQL/JSON executor functions. + */ +Datum +jsonb_from_text(text *js, bool unique_keys) +{ + return jsonb_from_cstring(VARDATA_ANY(js), + VARSIZE_ANY_EXHDR(js), + unique_keys, + NULL); +} + /* * Get the type name of a jsonb container. */ @@ -234,7 +248,7 @@ jsonb_typeof(PG_FUNCTION_ARGS) * instead of being thrown. */ static inline Datum -jsonb_from_cstring(char *json, int len, Node *escontext) +jsonb_from_cstring(char *json, int len, bool unique_keys, Node *escontext) { JsonLexContext *lex; JsonbInState state; @@ -244,6 +258,7 @@ jsonb_from_cstring(char *json, int len, Node *escontext) memset(&sem, 0, sizeof(sem)); lex = makeJsonLexContextCstringLen(json, len, GetDatabaseEncoding(), true); + state.unique_keys = unique_keys; state.escontext = escontext; sem.semstate = (void *) &state; @@ -280,6 +295,7 @@ jsonb_in_object_start(void *pstate) JsonbInState *_state = (JsonbInState *) pstate; _state->res = pushJsonbValue(&_state->parseState, WJB_BEGIN_OBJECT, NULL); + _state->parseState->unique_keys = _state->unique_keys; return JSON_SUCCESS; } @@ -1021,6 +1037,23 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, datum_to_jsonb(val, is_null, result, tcategory, outfuncoid, key_scalar); } + +/* + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. + */ +Datum +to_jsonb_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid) +{ + JsonbInState result; + + memset(&result, 0, sizeof(JsonbInState)); + + datum_to_jsonb(val, false, &result, tcategory, outfuncoid, false); + + return JsonbPGetDatum(JsonbValueToJsonb(result.res)); +} + /* * Is the given type immutable when coming out of a JSONB context? * @@ -1072,7 +1105,6 @@ to_jsonb(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - JsonbInState result; JsonTypeCategory tcategory; Oid outfuncoid; @@ -1084,11 +1116,7 @@ to_jsonb(PG_FUNCTION_ARGS) json_categorize_type(val_type, true, &tcategory, &outfuncoid); - memset(&result, 0, sizeof(JsonbInState)); - - datum_to_jsonb(val, false, &result, tcategory, outfuncoid, false); - - PG_RETURN_POINTER(JsonbValueToJsonb(result.res)); + PG_RETURN_DATUM(to_jsonb_worker(val, tcategory, outfuncoid)); } Datum diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d3a973d86b..d1b03d6cb2 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -10832,6 +10832,15 @@ get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, case JSCTOR_JSON_ARRAY: funcname = "JSON_ARRAY"; break; + case JSCTOR_JSON_PARSE: + funcname = "JSON"; + break; + case JSCTOR_JSON_SCALAR: + funcname = "JSON_SCALAR"; + break; + case JSCTOR_JSON_SERIALIZE: + funcname = "JSON_SERIALIZE"; + break; default: elog(ERROR, "invalid JsonConstructorType %d", ctor->type); } @@ -10879,7 +10888,10 @@ get_json_constructor_options(JsonConstructorExpr *ctor, StringInfo buf) if (ctor->unique) appendStringInfoString(buf, " WITH UNIQUE KEYS"); - get_json_returning(ctor->returning, buf, true); + if (!((ctor->type == JSCTOR_JSON_PARSE || + ctor->type == JSCTOR_JSON_SCALAR) && + ctor->returning->typid == JSONOID)) + get_json_returning(ctor->returning, buf, true); } /* diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index efb5c3e098..d926713bd9 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1716,6 +1716,17 @@ typedef struct TriggerTransition /* Nodes for SQL/JSON support */ +/* + * JsonQuotes - + * representation of [KEEP|OMIT] QUOTES clause for JSON_QUERY() + */ +typedef enum JsonQuotes +{ + JS_QUOTES_UNSPEC, /* unspecified */ + JS_QUOTES_KEEP, /* KEEP QUOTES */ + JS_QUOTES_OMIT /* OMIT QUOTES */ +} JsonQuotes; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1739,6 +1750,43 @@ typedef struct JsonKeyValue JsonValueExpr *value; /* JSON value expression */ } JsonKeyValue; +/* + * JsonParseExpr - + * untransformed representation of JSON() + */ +typedef struct JsonParseExpr +{ + NodeTag type; + JsonValueExpr *expr; /* string expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + bool unique_keys; /* WITH UNIQUE KEYS? */ + int location; /* token location, or -1 if unknown */ +} JsonParseExpr; + +/* + * JsonScalarExpr - + * untransformed representation of JSON_SCALAR() + */ +typedef struct JsonScalarExpr +{ + NodeTag type; + Expr *expr; /* scalar expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonScalarExpr; + +/* + * JsonSerializeExpr - + * untransformed representation of JSON_SERIALIZE() function + */ +typedef struct JsonSerializeExpr +{ + NodeTag type; + JsonValueExpr *expr; /* json value expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonSerializeExpr; + /* * JsonObjectConstructor - * untransformed representation of JSON_OBJECT() constructor diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 442d675600..42adaffc5d 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1610,7 +1610,10 @@ typedef enum JsonConstructorType JSCTOR_JSON_OBJECT = 1, JSCTOR_JSON_ARRAY = 2, JSCTOR_JSON_OBJECTAGG = 3, - JSCTOR_JSON_ARRAYAGG = 4 + JSCTOR_JSON_ARRAYAGG = 4, + JSCTOR_JSON_SCALAR = 5, + JSCTOR_JSON_SERIALIZE = 6, + JSCTOR_JSON_PARSE = 7 } JsonConstructorType; /* diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index f5b2e61ca5..5984dcfa4b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -230,11 +230,13 @@ PG_KEYWORD("is", IS, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("isnull", ISNULL, TYPE_FUNC_NAME_KEYWORD, AS_LABEL) PG_KEYWORD("isolation", ISOLATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) -PG_KEYWORD("json", JSON, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index 649a1644f2..f928e6142a 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -368,7 +368,6 @@ typedef struct JsonbIterator struct JsonbIterator *parent; } JsonbIterator; - /* Convenience macros */ static inline Jsonb * DatumGetJsonbP(Datum d) @@ -418,6 +417,7 @@ extern void JsonbHashScalarValueExtended(const JsonbValue *scalarVal, uint64 *hash, uint64 seed); /* jsonb.c support functions */ +extern Datum jsonb_from_text(text *js, bool unique_keys); extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index 6e9ea2df93..973e51f958 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -83,5 +83,9 @@ typedef enum void json_categorize_type(Oid typoid, bool is_jsonb, JsonTypeCategory *tcategory, Oid *outfuncoid); +extern Datum to_json_worker(Datum val, JsonTypeCategory tcategory, + Oid outfuncoid); +extern Datum to_jsonb_worker(Datum val, JsonTypeCategory tcategory, + Oid outfuncoid); #endif diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index d73c7e2c6c..ddea8a072f 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -1,3 +1,345 @@ +-- JSON() +SELECT JSON(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON(); + ^ +SELECT JSON(NULL); + json +------ + +(1 row) + +SELECT JSON('{ "a" : 1 } '); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +ERROR: JSON ENCODING clause is only allowed for bytea input type +LINE 1: SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); + ^ +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + pg_typeof +----------- + json +(1 row) + +SELECT JSON(' 1 '::json); + json +--------- + 1 +(1 row) + +SELECT JSON(' 1 '::jsonb); + json +------ + 1 +(1 row) + +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +ERROR: cannot use non-string types with WITH UNIQUE KEYS clause +LINE 1: SELECT JSON(' 1 '::json WITH UNIQUE KEYS); + ^ +SELECT JSON(123); +ERROR: cannot cast type integer to json +LINE 1: SELECT JSON(123); + ^ +SELECT JSON('{"a": 1, "a": 2}'); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +ERROR: duplicate JSON object key value +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); + QUERY PLAN +----------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); + QUERY PLAN +------------------------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON ENCODING UTF8) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); + QUERY PLAN +---------------------------------------------- + Result + Output: JSON('123'::text WITH UNIQUE KEYS) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +SELECT JSON('123' RETURNING text); +ERROR: cannot use RETURNING type text in JSON() +LINE 1: SELECT JSON('123' RETURNING text); + ^ +SELECT JSON('123' RETURNING text FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON() +LINE 1: SELECT JSON('123' RETURNING text FORMAT JSON); + ^ +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING json); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING jsonb); + QUERY PLAN +---------------------------------------------- + Result + Output: JSON('123'::jsonb RETURNING jsonb) +(2 rows) + +SELECT pg_typeof(JSON('123')); + pg_typeof +----------- + json +(1 row) + +SELECT pg_typeof(JSON('123' RETURNING json)); + pg_typeof +----------- + json +(1 row) + +SELECT pg_typeof(JSON('123' RETURNING jsonb)); + pg_typeof +----------- + jsonb +(1 row) + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SCALAR(); + ^ +SELECT JSON_SCALAR(NULL); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(NULL::int); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(123); + json_scalar +------------- + 123 +(1 row) + +SELECT JSON_SCALAR(123.45); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(123.45::numeric); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(true); + json_scalar +------------- + true +(1 row) + +SELECT JSON_SCALAR(false); + json_scalar +------------- + false +(1 row) + +SELECT JSON_SCALAR(' 123.45'); + json_scalar +------------- + " 123.45" +(1 row) + +SELECT JSON_SCALAR('2020-06-07'::date); + json_scalar +-------------- + "2020-06-07" +(1 row) + +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); + json_scalar +----------------------- + "2020-06-07T01:02:03" +(1 row) + +SELECT JSON_SCALAR('{}'::json); + json_scalar +------------- + {} +(1 row) + +SELECT JSON_SCALAR('{}'::jsonb); + json_scalar +------------- + {} +(1 row) + +SELECT JSON_SCALAR(123 RETURNING jsonb FORMAT JSON ENCODING UTF8); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_SCALAR() +LINE 1: SELECT JSON_SCALAR(123 RETURNING jsonb FORMAT JSON ENCODING ... + ^ +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); + QUERY PLAN +---------------------------- + Result + Output: JSON_SCALAR(123) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); + QUERY PLAN +------------------------------------ + Result + Output: JSON_SCALAR('123'::text) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING json); + QUERY PLAN +---------------------------- + Result + Output: JSON_SCALAR(123) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING jsonb); + QUERY PLAN +-------------------------------------------- + Result + Output: JSON_SCALAR(123 RETURNING jsonb) +(2 rows) + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SERIALIZE(); + ^ +SELECT JSON_SERIALIZE(NULL); + json_serialize +---------------- + +(1 row) + +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } '); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('1'); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('1' FORMAT JSON); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); + json_serialize +---------------------------- + \x7b20226122203a2031207d20 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + pg_typeof +----------- + text +(1 row) + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); +ERROR: cannot use RETURNING type jsonb in JSON_SERIALIZE() +HINT: Try returning a string type or bytea. +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); + QUERY PLAN +----------------------------------------------------- + Result + Output: JSON_SERIALIZE('{}'::json RETURNING text) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + QUERY PLAN +------------------------------------------------------ + Result + Output: JSON_SERIALIZE('{}'::json RETURNING bytea) +(2 rows) + -- JSON_OBJECT() SELECT JSON_OBJECT(); json_object @@ -630,6 +972,13 @@ ERROR: duplicate JSON object key SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); ERROR: duplicate JSON object key +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + json_objectagg +------------------ + {"1": 1, "2": 2} +(1 row) + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -645,6 +994,41 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); CREATE OR REPLACE VIEW public.json_object_view AS SELECT JSON_OBJECT('foo' : '1'::text FORMAT JSON, 'bar' : 'baz'::text RETURNING json) AS "json_object" DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + a | json_objectagg +---------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(2 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":1,"v":null} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1, "2" : 2 } + {"k":1,"v":null} | { "1" : 1, "2" : 2 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 4fd820fd51..f9afae0d42 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -1,3 +1,77 @@ +-- JSON() +SELECT JSON(); +SELECT JSON(NULL); +SELECT JSON('{ "a" : 1 } '); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + +SELECT JSON(' 1 '::json); +SELECT JSON(' 1 '::jsonb); +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +SELECT JSON(123); + +SELECT JSON('{"a": 1, "a": 2}'); +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + +SELECT JSON('123' RETURNING text); +SELECT JSON('123' RETURNING text FORMAT JSON); -- RETURNING FORMAT not allowed + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING json); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING jsonb); +SELECT pg_typeof(JSON('123')); +SELECT pg_typeof(JSON('123' RETURNING json)); +SELECT pg_typeof(JSON('123' RETURNING jsonb)); + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +SELECT JSON_SCALAR(NULL); +SELECT JSON_SCALAR(NULL::int); +SELECT JSON_SCALAR(123); +SELECT JSON_SCALAR(123.45); +SELECT JSON_SCALAR(123.45::numeric); +SELECT JSON_SCALAR(true); +SELECT JSON_SCALAR(false); +SELECT JSON_SCALAR(' 123.45'); +SELECT JSON_SCALAR('2020-06-07'::date); +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); +SELECT JSON_SCALAR('{}'::json); +SELECT JSON_SCALAR('{}'::jsonb); +SELECT JSON_SCALAR(123 RETURNING jsonb FORMAT JSON ENCODING UTF8); -- RETURNING FORMAT not allowed + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING json); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING jsonb); + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +SELECT JSON_SERIALIZE(NULL); +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); +SELECT JSON_SERIALIZE('{ "a" : 1 } '); +SELECT JSON_SERIALIZE('1'); +SELECT JSON_SERIALIZE('1' FORMAT JSON); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); + + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + -- JSON_OBJECT() SELECT JSON_OBJECT(); SELECT JSON_OBJECT(RETURNING json); @@ -216,6 +290,9 @@ FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -227,6 +304,24 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index b10590a252..d251fb9a91 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1296,6 +1296,7 @@ JsonPathPredicateCallback JsonPathString JsonReturning JsonSemAction +JsonSerializeExpr JsonTokenType JsonTransformStringValuesAction JsonTypeCategory -- 2.35.3
v4-0007-Claim-SQL-standard-compliance-for-SQL-JSON-featur.patch (application/octet-stream, 2.3K) download From 2cbf1c11ba24676680a9709dd5abff9a8e330fc8 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:16:39 +0900 Subject: [PATCH v4 7/7] Claim SQL standard compliance for SQL/JSON features Discussion: https://postgr.es/m/d03d809c-d0fb-fd6a-1476-d6dc18ec940e@dunslane.net --- src/backend/catalog/sql_features.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt index b33065d7bf..b379c71df9 100644 --- a/src/backend/catalog/sql_features.txt +++ b/src/backend/catalog/sql_features.txt @@ -548,15 +548,15 @@ T811 Basic SQL/JSON constructor functions YES T812 SQL/JSON: JSON_OBJECTAGG YES T813 SQL/JSON: JSON_ARRAYAGG with ORDER BY YES T814 Colon in JSON_OBJECT or JSON_OBJECTAGG YES -T821 Basic SQL/JSON query operators NO +T821 Basic SQL/JSON query operators YES T822 SQL/JSON: IS JSON WITH UNIQUE KEYS predicate YES -T823 SQL/JSON: PASSING clause NO -T824 JSON_TABLE: specific PLAN clause NO -T825 SQL/JSON: ON EMPTY and ON ERROR clauses NO -T826 General value expression in ON ERROR or ON EMPTY clauses NO -T827 JSON_TABLE: sibling NESTED COLUMNS clauses NO -T828 JSON_QUERY NO -T829 JSON_QUERY: array wrapper options NO +T823 SQL/JSON: PASSING clause YES +T824 JSON_TABLE: specific PLAN clause YES +T825 SQL/JSON: ON EMPTY and ON ERROR clauses YES +T826 General value expression in ON ERROR or ON EMPTY clauses YES +T827 JSON_TABLE: sibling NESTED COLUMNS clauses YES +T828 JSON_QUERY YES +T829 JSON_QUERY: array wrapper options YES T830 Enforcing unique keys in SQL/JSON constructor functions YES T831 SQL/JSON path language: strict mode YES T832 SQL/JSON path language: item method YES @@ -565,7 +565,7 @@ T834 SQL/JSON path language: wildcard member accessor YES T835 SQL/JSON path language: filter expressions YES T836 SQL/JSON path language: starts with predicate YES T837 SQL/JSON path language: regex_like predicate YES -T838 JSON_TABLE: PLAN DEFAULT clause NO +T838 JSON_TABLE: PLAN DEFAULT clause YES T839 Formatted cast of datetimes to/from character strings NO T840 Hex integer literals in SQL/JSON path language YES T851 SQL/JSON: optional keywords for default syntax YES -- 2.35.3
v4-0006-JSON_TABLE.patch (application/octet-stream, 160.4K) download From 58528ca8f450a192c6b882ab4e07b98c596fd91b Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:14:12 +0900 Subject: [PATCH v4 6/7] JSON_TABLE This feature allows jsonb data to be treated as a table and thus used in a FROM clause like other tabular data. Data can be selected from the jsonb using jsonpath expressions, and hoisted out of nested structures in the jsonb to form multiple rows, more or less like an outer join. This also adds the PLAN clauses for JSON_TABLE, which allow the user to specify how data from nested paths are joined, allowing considerable freedom in shaping the tabular output of JSON_TABLE. PLAN DEFAULT allows the user to specify the global strategies when dealing with sibling or child nested paths. The is often sufficient to achieve the necessary goal, and is considerably simpler than the full PLAN clause, which allows the user to specify the strategy to be used for each named nested path. Nikita Glukhov Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zhihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby. Discussion: https://postgr.es/m/7e2cb85d-24cf-4abb-30a5-1a33715959bd@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org --- doc/src/sgml/func.sgml | 448 ++++++++ src/backend/commands/explain.c | 8 +- src/backend/executor/execExprInterp.c | 5 + src/backend/executor/nodeTableFuncscan.c | 27 +- src/backend/nodes/makefuncs.c | 19 + src/backend/nodes/nodeFuncs.c | 30 + src/backend/parser/Makefile | 1 + src/backend/parser/gram.y | 476 ++++++++- src/backend/parser/meson.build | 1 + src/backend/parser/parse_clause.c | 14 +- src/backend/parser/parse_expr.c | 35 +- src/backend/parser/parse_jsontable.c | 739 +++++++++++++ src/backend/parser/parse_relation.c | 5 +- src/backend/parser/parse_target.c | 3 + src/backend/utils/adt/jsonpath_exec.c | 543 ++++++++++ src/backend/utils/adt/ruleutils.c | 279 ++++- src/include/nodes/execnodes.h | 2 + src/include/nodes/makefuncs.h | 2 + src/include/nodes/parsenodes.h | 90 ++ src/include/nodes/primnodes.h | 61 +- src/include/parser/kwlist.h | 4 + src/include/parser/parse_clause.h | 3 + src/include/utils/jsonpath.h | 3 + src/test/regress/expected/json_sqljson.out | 6 + src/test/regress/expected/jsonb_sqljson.out | 1069 +++++++++++++++++++ src/test/regress/sql/json_sqljson.sql | 4 + src/test/regress/sql/jsonb_sqljson.sql | 629 +++++++++++ src/tools/pgindent/typedefs.list | 13 + 28 files changed, 4486 insertions(+), 33 deletions(-) create mode 100644 src/backend/parser/parse_jsontable.c diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index cb36e1463b..44f016369e 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -17144,6 +17144,454 @@ array w/o UK? | t </sect2> + <sect2 id="functions-sqljson-table"> + <title>JSON_TABLE</title> + <indexterm> + <primary>json_table</primary> + </indexterm> + + <para> + <function>json_table</function> is an SQL/JSON function which + queries <acronym>JSON</acronym> data + and presents the results as a relational view, which can be accessed as a + regular SQL table. You can only use <function>json_table</function> inside the + <literal>FROM</literal> clause of a <literal>SELECT</literal> statement. + </para> + + <para> + Taking JSON data as input, <function>json_table</function> uses + a path expression to extract a part of the provided data that + will be used as a <firstterm>row pattern</firstterm> for the + constructed view. Each SQL/JSON item at the top level of the row pattern serves + as the source for a separate row in the constructed relational view. + </para> + + <para> + To split the row pattern into columns, <function>json_table</function> + provides the <literal>COLUMNS</literal> clause that defines the + schema of the created view. For each column to be constructed, + this clause provides a separate path expression that evaluates + the row pattern, extracts a JSON item, and returns it as a + separate SQL value for the specified column. If the required value + is stored in a nested level of the row pattern, it can be extracted + using the <literal>NESTED PATH</literal> subclause. Joining the + columns returned by <literal>NESTED PATH</literal> can add multiple + new rows to the constructed view. Such rows are called + <firstterm>child rows</firstterm>, as opposed to the <firstterm>parent row</firstterm> + that generates them. + </para> + + <para> + The rows produced by <function>JSON_TABLE</function> are laterally + joined to the row that generated them, so you do not have to explicitly join + the constructed view with the original table holding <acronym>JSON</acronym> + data. Optionally, you can specify how to join the columns returned + by <literal>NESTED PATH</literal> using the <literal>PLAN</literal> clause. + </para> + + <para> + Each <literal>NESTED PATH</literal> clause can generate one or more + columns. Columns produced by <literal>NESTED PATH</literal>s at the + same level are considered to be <firstterm>siblings</firstterm>, + while a column produced by a <literal>NESTED PATH</literal> is + considered to be a child of the column produced by a + <literal>NESTED PATH</literal> or row expression at a higher level. + Sibling columns are always joined first. Once they are processed, + the resulting rows are joined to the parent row. + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional></literal> + </term> + <listitem> + <para> + The input data to query, the JSON path expression defining the query, + and an optional <literal>PASSING</literal> clause, which can provide data + values to the <replaceable>path_expression</replaceable>. + The result of the input data + evaluation is called the <firstterm>row pattern</firstterm>. The row + pattern is used as the source for row values in the constructed view. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>COLUMNS</literal>( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + The <literal>COLUMNS</literal> clause defining the schema of the + constructed view. In this clause, you must specify all the columns + to be filled with SQL/JSON items. + The <replaceable>json_table_column</replaceable> + expression has the following syntax variants: + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>name</replaceable> <replaceable>type</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional></literal> + </term> + <listitem> + + <para> + Inserts a single SQL/JSON item into each row of + the specified column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable> + and fills the column with produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>ON EMPTY</literal> and + <literal>ON ERROR</literal> clauses to define how to handle missing values + or structural errors. + <literal>WRAPPER</literal> and <literal>QUOTES</literal> clauses can only + be used with JSON, array, and composite types. + These clauses have the same syntax and semantics as for + <function>json_value</function> and <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> <literal>FORMAT</literal> <replaceable>json_representation</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a composite SQL/JSON + item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable> + and fills the column with produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>WRAPPER</literal>, <literal>QUOTES</literal>, + <literal>ON EMPTY</literal> and <literal>ON ERROR</literal> clauses + to define additional settings for the returned SQL/JSON items. + These clauses have the same syntax and semantics as + for <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> + <literal>EXISTS</literal> <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a boolean item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable>, + checks whether any SQL/JSON items were returned, and fills the column with + resulting boolean value, one for each row. + The specified <replaceable>type</replaceable> should have cast from + <type>boolean</type>. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + </para> + <para> + Optionally, you can add <literal>ON ERROR</literal> clause to define + error behavior. This clause has the same syntax and semantics as + for <function>json_exists</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>NESTED PATH</literal> <replaceable>json_path_specification</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> + <literal>COLUMNS</literal> ( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + Extracts SQL/JSON items from nested levels of the row pattern, + generates one or more columns as defined by the <literal>COLUMNS</literal> + subclause, and inserts the extracted SQL/JSON items into each row of these columns. + The <replaceable>json_table_column</replaceable> expression in the + <literal>COLUMNS</literal> subclause uses the same syntax as in the + parent <literal>COLUMNS</literal> clause. + </para> + + <para> + The <literal>NESTED PATH</literal> syntax is recursive, + so you can go down multiple nested levels by specifying several + <literal>NESTED PATH</literal> subclauses within each other. + It allows to unnest the hierarchy of JSON objects and arrays + in a single function invocation rather than chaining several + <function>JSON_TABLE</function> expressions in an SQL statement. + </para> + + <para> + You can use the <literal>PLAN</literal> clause to define how + to join the columns returned by <literal>NESTED PATH</literal> clauses. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <literal>FOR ORDINALITY</literal> + </term> + <listitem> + + <para> + Adds an ordinality column that provides sequential row numbering. + You can have only one ordinality column per table. Row numbering + is 1-based. For child rows that result from the <literal>NESTED PATH</literal> + clauses, the parent row number is repeated. + </para> + </listitem> + </varlistentry> + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>AS</literal> <replaceable>json_path_name</replaceable> + </term> + <listitem> + + <para> + The optional <replaceable>json_path_name</replaceable> serves as an + identifier of the provided <replaceable>json_path_specification</replaceable>. + The path name must be unique and distinct from the column names. + When using the <literal>PLAN</literal> clause, you must specify the names + for all the paths, including the row pattern. Each path name can appear in + the <literal>PLAN</literal> clause only once. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN</literal> ( <replaceable>json_table_plan</replaceable> ) + </term> + <listitem> + + <para> + Defines how to join the data returned by <literal>NESTED PATH</literal> + clauses to the constructed view. + </para> + <para> + To join columns with parent/child relationship, you can use: + </para> + <variablelist> + <varlistentry> + <term> + <literal>INNER</literal> + </term> + <listitem> + + <para> + Use <literal>INNER JOIN</literal>, so that the parent row + is omitted from the output if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>OUTER</literal> + </term> + <listitem> + + <para> + Use <literal>LEFT OUTER JOIN</literal>, so that the parent row + is always included into the output even if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>, with NULL values + inserted into the child columns if the corresponding + values are missing. + </para> + <para> + This is the default option for joining columns with parent/child relationship. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para> + To join sibling columns, you can use: + </para> + + <variablelist> + <varlistentry> + <term> + <literal>UNION</literal> + </term> + <listitem> + + <para> + Generate one row for each value produced by each of the sibling + columns. The columns from the other siblings are set to null. + </para> + <para> + This is the default option for joining sibling columns. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>CROSS</literal> + </term> + <listitem> + + <para> + Generate one row for each combination of values from the sibling columns. + </para> + </listitem> + </varlistentry> + + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN DEFAULT</literal> ( <literal><replaceable>OUTER | INNER</replaceable> <optional>, <replaceable>UNION | CROSS</replaceable> </optional></literal> ) + </term> + <listitem> + <para> + The terms can also be specified in reverse order. The + <literal>INNER</literal> or <literal>OUTER</literal> option defines the + joining plan for parent/child columns, while <literal>UNION</literal> or + <literal>CROSS</literal> affects joins of sibling columns. This form + of <literal>PLAN</literal> overrides the default plan for + all columns at once. Even though the path names are not included in the + <literal>PLAN DEFAULT</literal> form, to conform to the SQL/JSON standard + they must be provided for all the paths if the <literal>PLAN</literal> + clause is used. + </para> + <para> + <literal>PLAN DEFAULT</literal> is simpler than specifying a complete + <literal>PLAN</literal>, and is often all that is required to get the desired + output. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para>Examples</para> + + <para> + In these examples the following small table storing some JSON data will be used: +<programlisting> +CREATE TABLE my_films ( js jsonb ); + +INSERT INTO my_films VALUES ( +'{ "favorites" : [ + { "kind" : "comedy", "films" : [ + { "title" : "Bananas", + "director" : "Woody Allen"}, + { "title" : "The Dinner Game", + "director" : "Francis Veber" } ] }, + { "kind" : "horror", "films" : [ + { "title" : "Psycho", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "thriller", "films" : [ + { "title" : "Vertigo", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "drama", "films" : [ + { "title" : "Yojimbo", + "director" : "Akira Kurosawa" } ] } + ] }'); +</programlisting> + </para> + <para> + Query the <structname>my_films</structname> table holding + some JSON data about the films and create a view that + distributes the film genre, title, and director between separate columns: +<screen> +SELECT jt.* FROM + my_films, + JSON_TABLE ( js, '$.favorites[*]' COLUMNS ( + id FOR ORDINALITY, + kind text PATH '$.kind', + NESTED PATH '$.films[*]' COLUMNS ( + title text PATH '$.title', + director text PATH '$.director'))) AS jt; +----+----------+------------------+------------------- + id | kind | title | director +----+----------+------------------+------------------- + 1 | comedy | Bananas | Woody Allen + 1 | comedy | The Dinner Game | Francis Veber + 2 | horror | Psycho | Alfred Hitchcock + 3 | thriller | Vertigo | Alfred Hitchcock + 4 | drama | Yojimbo | Akira Kurosawa + (5 rows) +</screen> + </para> + + <para> + Find a director that has done films in two different genres: +<screen> +SELECT + director1 AS director, title1, kind1, title2, kind2 +FROM + my_films, + JSON_TABLE ( js, '$.favorites' AS favs COLUMNS ( + NESTED PATH '$[*]' AS films1 COLUMNS ( + kind1 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film1 COLUMNS ( + title1 text PATH '$.title', + director1 text PATH '$.director') + ), + NESTED PATH '$[*]' AS films2 COLUMNS ( + kind2 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film2 COLUMNS ( + title2 text PATH '$.title', + director2 text PATH '$.director' + ) + ) + ) + PLAN (favs OUTER ((films1 INNER film1) CROSS (films2 INNER film2))) + ) AS jt + WHERE kind1 > kind2 AND director1 = director2; + + director | title1 | kind1 | title2 | kind2 +------------------+---------+----------+--------+-------- + Alfred Hitchcock | Vertigo | thriller | Psycho | horror +(1 row) +</screen> + </para> + </sect2> + <sect2 id="functions-sqljson-path"> <title>The SQL/JSON Path Language</title> diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 8570b14f62..ad899de5d6 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -3862,7 +3862,13 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es) break; case T_TableFuncScan: Assert(rte->rtekind == RTE_TABLEFUNC); - objectname = "xmltable"; + if (rte->tablefunc) + if (rte->tablefunc->functype == TFT_XMLTABLE) + objectname = "xmltable"; + else /* Must be TFT_JSON_TABLE */ + objectname = "json_table"; + else + objectname = NULL; objecttag = "Table Function Name"; break; case T_ValuesScan: diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 4c97e714ea..84c8730129 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -4309,6 +4309,11 @@ ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) break; } + case JSON_TABLE_OP: + res = item; + resnull = false; + break; + default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); *op->resnull = true; diff --git a/src/backend/executor/nodeTableFuncscan.c b/src/backend/executor/nodeTableFuncscan.c index 791cbd2372..085a612533 100644 --- a/src/backend/executor/nodeTableFuncscan.c +++ b/src/backend/executor/nodeTableFuncscan.c @@ -28,6 +28,7 @@ #include "miscadmin.h" #include "nodes/execnodes.h" #include "utils/builtins.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/xml.h" @@ -161,8 +162,9 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) scanstate->ss.ps.qual = ExecInitQual(node->scan.plan.qual, &scanstate->ss.ps); - /* Only XMLTABLE is supported currently */ - scanstate->routine = &XmlTableRoutine; + /* Only XMLTABLE and JSON_TABLE are supported currently */ + scanstate->routine = + tf->functype == TFT_XMLTABLE ? &XmlTableRoutine : &JsonbTableRoutine; scanstate->perTableCxt = AllocSetContextCreate(CurrentMemoryContext, @@ -182,6 +184,10 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) ExecInitExprList(tf->colexprs, (PlanState *) scanstate); scanstate->coldefexprs = ExecInitExprList(tf->coldefexprs, (PlanState *) scanstate); + scanstate->colvalexprs = + ExecInitExprList(tf->colvalexprs, (PlanState *) scanstate); + scanstate->passingvalexprs = + ExecInitExprList(tf->passingvalexprs, (PlanState *) scanstate); scanstate->notnulls = tf->notnulls; @@ -381,14 +387,17 @@ tfuncInitialize(TableFuncScanState *tstate, ExprContext *econtext, Datum doc) routine->SetNamespace(tstate, ns_name, ns_uri); } - /* Install the row filter expression into the table builder context */ - value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); - if (isnull) - ereport(ERROR, - (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), - errmsg("row filter expression must not be null"))); + if (routine->SetRowFilter) + { + /* Install the row filter expression into the table builder context */ + value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); + if (isnull) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("row filter expression must not be null"))); - routine->SetRowFilter(tstate, TextDatumGetCString(value)); + routine->SetRowFilter(tstate, TextDatumGetCString(value)); + } /* * Install the column filter expressions into the table builder context. diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 7bcc12b04f..7a03283713 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -878,6 +878,25 @@ makeJsonBehavior(JsonBehaviorType type, Node *default_expr) return behavior; } +/* + * makeJsonTableJoinedPlan - + * creates a joined JsonTablePlan node + */ +Node * +makeJsonTableJoinedPlan(JsonTablePlanJoinType type, Node *plan1, Node *plan2, + int location) +{ + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_JOINED; + n->join_type = type; + n->plan1 = castNode(JsonTablePlan, plan1); + n->plan2 = castNode(JsonTablePlan, plan2); + n->location = location; + + return (Node *) n; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index d31371bb4d..e07c066e15 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -2631,6 +2631,10 @@ expression_tree_walker_impl(Node *node, return true; if (WALK(tf->coldefexprs)) return true; + if (WALK(tf->colvalexprs)) + return true; + if (WALK(tf->passingvalexprs)) + return true; } break; default: @@ -3699,6 +3703,8 @@ expression_tree_mutator_impl(Node *node, MUTATE(newnode->rowexpr, tf->rowexpr, Node *); MUTATE(newnode->colexprs, tf->colexprs, List *); MUTATE(newnode->coldefexprs, tf->coldefexprs, List *); + MUTATE(newnode->colvalexprs, tf->colvalexprs, List *); + MUTATE(newnode->passingvalexprs, tf->passingvalexprs, List *); return (Node *) newnode; } break; @@ -4130,6 +4136,30 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonTable: + { + JsonTable *jt = (JsonTable *) node; + + if (WALK(jt->common)) + return true; + if (WALK(jt->columns)) + return true; + } + break; + case T_JsonTableColumn: + { + JsonTableColumn *jtc = (JsonTableColumn *) node; + + if (WALK(jtc->typeName)) + return true; + if (WALK(jtc->on_empty)) + return true; + if (WALK(jtc->on_error)) + return true; + if (jtc->coltype == JTC_NESTED && WALK(jtc->columns)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile index 9f1c4022bb..f4c0cc7f10 100644 --- a/src/backend/parser/Makefile +++ b/src/backend/parser/Makefile @@ -23,6 +23,7 @@ OBJS = \ parse_enr.o \ parse_expr.o \ parse_func.o \ + parse_jsontable.o \ parse_merge.o \ parse_node.o \ parse_oper.o \ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index f7ad8f18de..ff1d3332c1 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -654,19 +654,43 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_aggregate_func json_api_common_syntax json_argument + json_table + json_table_column_definition + json_table_ordinality_column_definition + json_table_regular_column_definition + json_table_formatted_column_definition + json_table_exists_column_definition + json_table_nested_columns + json_table_plan_clause_opt + json_table_plan + json_table_plan_simple + json_table_plan_parent_child + json_table_plan_outer + json_table_plan_inner + json_table_plan_sibling + json_table_plan_union + json_table_plan_cross + json_table_plan_primary %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt json_arguments + json_table_columns_clause + json_table_column_definition_list +%type <str> json_table_column_path_specification_clause_opt %type <ival> json_encoding_clause_opt json_predicate_type_constraint json_wrapper_behavior + json_table_default_plan_choices + json_table_default_plan_inner_outer + json_table_default_plan_union_cross %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt %type <jsbehavior> json_value_behavior json_query_behavior json_exists_behavior + json_table_behavior %type <js_quotes> json_quotes_clause_opt /* @@ -732,7 +756,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG - JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_TABLE JSON_VALUE KEY KEYS KEEP @@ -743,8 +767,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE - NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE - NORMALIZE NORMALIZED + NAME_P NAMES NATIONAL NATURAL NCHAR NESTED NEW NEXT NFC NFD NFKC NFKD NO + NONE NORMALIZE NORMALIZED NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC @@ -752,8 +776,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER - PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD - PLACING PLANS POLICY + PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH + PLACING PLAN PLANS POLICY POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION @@ -861,6 +885,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); * Using the same precedence as IDENT seems right for the reasons given above. */ %nonassoc UNBOUNDED /* ideally would have same precedence as IDENT */ +%nonassoc COLUMNS %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' @@ -883,6 +908,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL +%nonassoc json_table_column +%nonassoc NESTED +%left PATH %% /* @@ -13324,6 +13352,21 @@ table_ref: relation_expr opt_alias_clause $2->alias = $4; $$ = (Node *) $2; } + | json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $1); + + jt->alias = $2; + $$ = (Node *) jt; + } + | LATERAL_P json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $2); + + jt->alias = $3; + jt->lateral = true; + $$ = (Node *) jt; + } ; @@ -13891,6 +13934,8 @@ xmltable_column_option_el: { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(true), @1); } | NULL_P { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(false), @1); } + | PATH b_expr + { $$ = makeDefElem("path", $2, @1); } ; xml_namespace_list: @@ -16697,6 +16742,11 @@ json_value_behavior: | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } ; +json_table_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + ; + /* ARRAY is a noise word */ json_wrapper_behavior: WITHOUT WRAPPER { $$ = JSW_NONE; } @@ -16718,6 +16768,414 @@ json_quotes_clause_opt: | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } ; +json_table: + JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + json_table_behavior ON ERROR_P + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_columns_clause: + COLUMNS '(' json_table_column_definition_list ')' { $$ = $3; } + ; + +json_table_column_definition_list: + json_table_column_definition + { $$ = list_make1($1); } + | json_table_column_definition_list ',' json_table_column_definition + { $$ = lappend($1, $3); } + ; + +json_table_column_definition: + json_table_ordinality_column_definition %prec json_table_column + | json_table_regular_column_definition %prec json_table_column + | json_table_formatted_column_definition %prec json_table_column + | json_table_exists_column_definition %prec json_table_column + | json_table_nested_columns + ; + +json_table_ordinality_column_definition: + ColId FOR ORDINALITY + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FOR_ORDINALITY; + n->name = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_regular_column_definition: + ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_exists_column_definition: + ColId Typename + EXISTS json_table_column_path_specification_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + EXISTS json_table_column_path_specification_clause_opt + json_exists_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_column_path_specification_clause_opt: + PATH Sconst { $$ = $2; } + | /* EMPTY */ %prec json_table_column { $$ = NULL; } + ; + +json_table_formatted_column_definition: + ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->on_error = $12; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_nested_columns: + NESTED path_opt Sconst + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = NULL; + n->columns = $4; + n->location = @1; + $$ = (Node *) n; + } + | NESTED path_opt Sconst AS name + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = $5; + n->columns = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +path_opt: + PATH { } + | /* EMPTY */ { } + ; + +json_table_plan_clause_opt: + PLAN '(' json_table_plan ')' { $$ = $3; } + | PLAN DEFAULT '(' json_table_default_plan_choices ')' + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_DEFAULT; + n->join_type = $4; + n->location = @1; + $$ = (Node *) n; + } + | /* EMPTY */ { $$ = NULL; } + ; + +json_table_plan: + json_table_plan_simple + | json_table_plan_parent_child + | json_table_plan_sibling + ; + +json_table_plan_simple: + name + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_SIMPLE; + n->pathname = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_plan_primary: + json_table_plan_simple { $$ = $1; } + | '(' json_table_plan ')' + { + castNode(JsonTablePlan, $2)->location = @1; + $$ = $2; + } + ; + +json_table_plan_outer: + json_table_plan_simple OUTER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_OUTER, $1, $3, @1); } + ; + +json_table_plan_inner: + json_table_plan_simple INNER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_INNER, $1, $3, @1); } + ; + +json_table_plan_parent_child: + json_table_plan_outer + | json_table_plan_inner + ; + +json_table_plan_union: + json_table_plan_primary UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + | json_table_plan_union UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + ; + +json_table_plan_cross: + json_table_plan_primary CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + | json_table_plan_cross CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + ; + +json_table_plan_sibling: + json_table_plan_union + | json_table_plan_cross + ; + +json_table_default_plan_choices: + json_table_default_plan_inner_outer { $$ = $1 | JSTPJ_UNION; } + | json_table_default_plan_union_cross { $$ = $1 | JSTPJ_OUTER; } + | json_table_default_plan_inner_outer ',' + json_table_default_plan_union_cross { $$ = $1 | $3; } + | json_table_default_plan_union_cross ',' + json_table_default_plan_inner_outer { $$ = $1 | $3; } + ; + +json_table_default_plan_inner_outer: + INNER_P { $$ = JSTPJ_INNER; } + | OUTER_P { $$ = JSTPJ_OUTER; } + ; + +json_table_default_plan_union_cross: + UNION { $$ = JSTPJ_UNION; } + | CROSS { $$ = JSTPJ_CROSS; } + ; + json_returning_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17442,6 +17900,7 @@ unreserved_keyword: | MOVE | NAME_P | NAMES + | NESTED | NEW | NEXT | NFC @@ -17476,6 +17935,8 @@ unreserved_keyword: | PARTITION | PASSING | PASSWORD + | PATH + | PLAN | PLANS | POLICY | PRECEDING @@ -17640,6 +18101,7 @@ col_name_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | LEAST | NATIONAL @@ -18008,6 +18470,7 @@ bare_label_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | KEEP | KEY @@ -18047,6 +18510,7 @@ bare_label_keyword: | NATIONAL | NATURAL | NCHAR + | NESTED | NEW | NEXT | NFC @@ -18091,7 +18555,9 @@ bare_label_keyword: | PARTITION | PASSING | PASSWORD + | PATH | PLACING + | PLAN | PLANS | POLICY | POSITION diff --git a/src/backend/parser/meson.build b/src/backend/parser/meson.build index 94517a7f7d..0f0e4ac7ca 100644 --- a/src/backend/parser/meson.build +++ b/src/backend/parser/meson.build @@ -10,6 +10,7 @@ backend_sources += files( 'parse_enr.c', 'parse_expr.c', 'parse_func.c', + 'parse_jsontable.c', 'parse_merge.c', 'parse_node.c', 'parse_oper.c', diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index f61f794755..b44ff44991 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -697,7 +697,11 @@ transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf) char **names; int colno; - /* Currently only XMLTABLE is supported */ + /* + * Currently we only support XMLTABLE here. See transformJsonTable() for + * JSON_TABLE support. + */ + tf->functype = TFT_XMLTABLE; constructName = "XMLTABLE"; docType = XMLOID; @@ -1104,13 +1108,17 @@ transformFromClauseItem(ParseState *pstate, Node *n, rtr->rtindex = nsitem->p_rtindex; return (Node *) rtr; } - else if (IsA(n, RangeTableFunc)) + else if (IsA(n, RangeTableFunc) || IsA(n, JsonTable)) { /* table function is like a plain relation */ RangeTblRef *rtr; ParseNamespaceItem *nsitem; - nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + if (IsA(n, RangeTableFunc)) + nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + else + nsitem = transformJsonTable(pstate, (JsonTable *) n); + *top_nsitem = nsitem; *namespace = list_make1(nsitem); rtr = makeNode(RangeTblRef); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 92eda6a960..d09582e111 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4253,7 +4253,7 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) JsonFormatType format; char *constructName; - if (func->common->pathname) + if (func->common->pathname && func->op != JSON_TABLE_OP) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("JSON_TABLE path name is not allowed here"), @@ -4272,6 +4272,9 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) case JSON_EXISTS_OP: constructName = "JSON_EXISTS()"; break; + case JSON_TABLE_OP: + constructName = "JSON_TABLE()"; + break; default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); break; @@ -4311,14 +4314,19 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) &jsexpr->passing_values, &jsexpr->passing_names); - if (func->op != JSON_EXISTS_OP) + if (func->op != JSON_EXISTS_OP && func->op != JSON_TABLE_OP) jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, JSON_BEHAVIOR_NULL); - jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, - func->op == JSON_EXISTS_OP ? - JSON_BEHAVIOR_FALSE : - JSON_BEHAVIOR_NULL); + if (func->op == JSON_EXISTS_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_FALSE); + else if (func->op == JSON_TABLE_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_EMPTY); + else + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_NULL); return jsexpr; } @@ -4638,6 +4646,21 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->result_coercion->expr = NULL; } break; + + case JSON_TABLE_OP: + jsexpr->returning = makeNode(JsonReturning); + jsexpr->returning->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + jsexpr->returning->typid = exprType(contextItemExpr); + jsexpr->returning->typmod = -1; + + if (jsexpr->returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("JSON_TABLE() is not yet implemented for the json type"), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + break; } if (exprType(contextItemExpr) != JSONBOID) diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c new file mode 100644 index 0000000000..a7802e0499 --- /dev/null +++ b/src/backend/parser/parse_jsontable.c @@ -0,0 +1,739 @@ +/*------------------------------------------------------------------------- + * + * parse_jsontable.c + * parsing of JSON_TABLE + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/parser/parse_jsontable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/pg_collation.h" +#include "catalog/pg_type.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "optimizer/optimizer.h" +#include "parser/parse_clause.h" +#include "parser/parse_collate.h" +#include "parser/parse_expr.h" +#include "parser/parse_relation.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/json.h" +#include "utils/lsyscache.h" + +/* Context for JSON_TABLE transformation */ +typedef struct JsonTableParseContext +{ + ParseState *pstate; /* parsing state */ + JsonTable *table; /* untransformed node */ + TableFunc *tablefunc; /* transformed node */ + List *pathNames; /* list of all path and columns names */ + int pathNameId; /* path name id counter */ + Oid contextItemTypid; /* type oid of context item (json/jsonb) */ +} JsonTableParseContext; + +static JsonTableParent *transformJsonTableColumns(JsonTableParseContext *cxt, + JsonTablePlan *plan, + List *columns, + char *pathSpec, + char **pathName, + int location); + +static Node * +makeStringConst(char *str, int location) +{ + A_Const *n = makeNode(A_Const); + + n->val.node.type = T_String; + n->val.sval.sval = str; + n->location = location; + + return (Node *) n; +} + +/* + * Transform JSON_TABLE column + * - regular column into JSON_VALUE() + * - FORMAT JSON column into JSON_QUERY() + * - EXISTS column into JSON_EXISTS() + */ +static Node * +transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, + List *passingArgs, bool errorOnError) +{ + JsonFuncExpr *jfexpr = makeNode(JsonFuncExpr); + JsonCommon *common = makeNode(JsonCommon); + JsonOutput *output = makeNode(JsonOutput); + char *pathspec; + JsonFormat *default_format; + + jfexpr->op = + jtc->coltype == JTC_REGULAR ? JSON_VALUE_OP : + jtc->coltype == JTC_EXISTS ? JSON_EXISTS_OP : JSON_QUERY_OP; + jfexpr->common = common; + jfexpr->output = output; + jfexpr->on_empty = jtc->on_empty; + jfexpr->on_error = jtc->on_error; + if (!jfexpr->on_error && errorOnError) + jfexpr->on_error = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); + jfexpr->omit_quotes = jtc->omit_quotes; + jfexpr->wrapper = jtc->wrapper; + jfexpr->location = jtc->location; + + output->typeName = jtc->typeName; + output->returning = makeNode(JsonReturning); + output->returning->format = jtc->format; + + default_format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + + common->pathname = NULL; + common->expr = makeJsonValueExpr((Expr *) contextItemExpr, default_format); + common->passing = passingArgs; + + if (jtc->pathspec) + pathspec = jtc->pathspec; + else + { + /* Construct default path as '$."column_name"' */ + StringInfoData path; + + initStringInfo(&path); + + appendStringInfoString(&path, "$."); + escape_json(&path, jtc->name); + + pathspec = path.data; + } + + common->pathspec = makeStringConst(pathspec, -1); + + return (Node *) jfexpr; +} + +static bool +isJsonTablePathNameDuplicate(JsonTableParseContext *cxt, const char *pathname) +{ + ListCell *lc; + + foreach(lc, cxt->pathNames) + { + if (!strcmp(pathname, (const char *) lfirst(lc))) + return true; + } + + return false; +} + +/* Register the column name in the path name list. */ +static void +registerJsonTableColumn(JsonTableParseContext *cxt, char *colname) +{ + if (isJsonTablePathNameDuplicate(cxt, colname)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_ALIAS), + errmsg("duplicate JSON_TABLE column name: %s", colname), + errhint("JSON_TABLE column names must be distinct from one another."))); + + cxt->pathNames = lappend(cxt->pathNames, colname); +} + +/* Recursively register all nested column names in the path name list. */ +static void +registerAllJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED) + { + if (jtc->pathname) + registerJsonTableColumn(cxt, jtc->pathname); + + registerAllJsonTableColumns(cxt, jtc->columns); + } + else + { + registerJsonTableColumn(cxt, jtc->name); + } + } +} + +/* Generate a new unique JSON_TABLE path name. */ +static char * +generateJsonTablePathName(JsonTableParseContext *cxt) +{ + char namebuf[32]; + char *name = namebuf; + + do + { + snprintf(namebuf, sizeof(namebuf), "json_table_path_%d", + ++cxt->pathNameId); + } while (isJsonTablePathNameDuplicate(cxt, name)); + + name = pstrdup(name); + cxt->pathNames = lappend(cxt->pathNames, name); + + return name; +} + +/* Collect sibling path names from plan to the specified list. */ +static void +collectSiblingPathsInJsonTablePlan(JsonTablePlan *plan, List **paths) +{ + if (plan->plan_type == JSTP_SIMPLE) + *paths = lappend(*paths, plan->pathname); + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + *paths = lappend(*paths, plan->plan1->pathname); + } + else if (plan->join_type == JSTPJ_CROSS || + plan->join_type == JSTPJ_UNION) + { + collectSiblingPathsInJsonTablePlan(plan->plan1, paths); + collectSiblingPathsInJsonTablePlan(plan->plan2, paths); + } + else + elog(ERROR, "invalid JSON_TABLE join type %d", + plan->join_type); + } +} + +/* + * Validate child JSON_TABLE plan by checking that: + * - all nested columns have path names specified + * - all nested columns have corresponding node in the sibling plan + * - plan does not contain duplicate or extra nodes + */ +static void +validateJsonTableChildPlan(ParseState *pstate, JsonTablePlan *plan, + List *columns) +{ + ListCell *lc1; + List *siblings = NIL; + int nchildren = 0; + + if (plan) + collectSiblingPathsInJsonTablePlan(plan, &siblings); + + foreach(lc1, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc1)); + + if (jtc->coltype == JTC_NESTED) + { + ListCell *lc2; + bool found = false; + + if (!jtc->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("nested JSON_TABLE columns must contain an explicit AS pathname specification if an explicit PLAN clause is used"), + parser_errposition(pstate, jtc->location))); + + /* find nested path name in the list of sibling path names */ + foreach(lc2, siblings) + { + if ((found = !strcmp(jtc->pathname, lfirst(lc2)))) + break; + } + + if (!found) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node for nested path %s was not found in plan.", jtc->pathname), + parser_errposition(pstate, jtc->location))); + + nchildren++; + } + } + + if (list_length(siblings) > nchildren) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node contains some extra or duplicate sibling nodes."), + parser_errposition(pstate, plan ? plan->location : -1))); +} + +static JsonTableColumn * +findNestedJsonTableColumn(List *columns, const char *pathname) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED && + jtc->pathname && + !strcmp(jtc->pathname, pathname)) + return jtc; + } + + return NULL; +} + +static Node * +transformNestedJsonTableColumn(JsonTableParseContext *cxt, JsonTableColumn *jtc, + JsonTablePlan *plan) +{ + JsonTableParent *node; + char *pathname = jtc->pathname; + + node = transformJsonTableColumns(cxt, plan, jtc->columns, jtc->pathspec, + &pathname, jtc->location); + + return (Node *) node; +} + +static Node * +makeJsonTableSiblingJoin(bool cross, Node *lnode, Node *rnode) +{ + JsonTableSibling *join = makeNode(JsonTableSibling); + + join->larg = lnode; + join->rarg = rnode; + join->cross = cross; + + return (Node *) join; +} + +/* + * Recursively transform child JSON_TABLE plan. + * + * Default plan is transformed into a cross/union join of its nested columns. + * Simple and outer/inner plans are transformed into a JsonTableParent by + * finding and transforming corresponding nested column. + * Sibling plans are recursively transformed into a JsonTableSibling. + */ +static Node * +transformJsonTableChildPlan(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns) +{ + JsonTableColumn *jtc = NULL; + + if (!plan || plan->plan_type == JSTP_DEFAULT) + { + /* unspecified or default plan */ + Node *res = NULL; + ListCell *lc; + bool cross = plan && (plan->join_type & JSTPJ_CROSS); + + /* transform all nested columns into cross/union join */ + foreach(lc, columns) + { + JsonTableColumn *col = castNode(JsonTableColumn, lfirst(lc)); + Node *node; + + if (col->coltype != JTC_NESTED) + continue; + + node = transformNestedJsonTableColumn(cxt, col, plan); + + /* join transformed node with previous sibling nodes */ + res = res ? makeJsonTableSiblingJoin(cross, res, node) : node; + } + + return res; + } + else if (plan->plan_type == JSTP_SIMPLE) + { + jtc = findNestedJsonTableColumn(columns, plan->pathname); + } + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + jtc = findNestedJsonTableColumn(columns, plan->plan1->pathname); + } + else + { + Node *node1 = transformJsonTableChildPlan(cxt, plan->plan1, + columns); + Node *node2 = transformJsonTableChildPlan(cxt, plan->plan2, + columns); + + return makeJsonTableSiblingJoin(plan->join_type == JSTPJ_CROSS, + node1, node2); + } + } + else + elog(ERROR, "invalid JSON_TABLE plan type %d", plan->plan_type); + + if (!jtc) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name was %s not found in nested columns list.", + plan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + return transformNestedJsonTableColumn(cxt, jtc, plan); +} + +/* Check whether type is json/jsonb, array, or record. */ +static bool +typeIsComposite(Oid typid) +{ + char typtype; + + if (typid == JSONOID || + typid == JSONBOID || + typid == RECORDOID || + type_is_array(typid)) + return true; + + typtype = get_typtype(typid); + + if (typtype == TYPTYPE_COMPOSITE) + return true; + + if (typtype == TYPTYPE_DOMAIN) + return typeIsComposite(getBaseType(typid)); + + return false; +} + +/* Append transformed non-nested JSON_TABLE columns to the TableFunc node */ +static void +appendJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *col; + ParseState *pstate = cxt->pstate; + JsonTable *jt = cxt->table; + TableFunc *tf = cxt->tablefunc; + bool errorOnError = jt->on_error && + jt->on_error->btype == JSON_BEHAVIOR_ERROR; + + foreach(col, columns) + { + JsonTableColumn *rawc = castNode(JsonTableColumn, lfirst(col)); + Oid typid; + int32 typmod; + Node *colexpr; + + if (rawc->name) + { + /* make sure column names are unique */ + ListCell *colname; + + foreach(colname, tf->colnames) + if (!strcmp((const char *) colname, rawc->name)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("column name \"%s\" is not unique", + rawc->name), + parser_errposition(pstate, rawc->location))); + + tf->colnames = lappend(tf->colnames, + makeString(pstrdup(rawc->name))); + } + + /* + * Determine the type and typmod for the new column. FOR ORDINALITY + * columns are INTEGER by standard; the others are user-specified. + */ + switch (rawc->coltype) + { + case JTC_FOR_ORDINALITY: + colexpr = NULL; + typid = INT4OID; + typmod = -1; + break; + + case JTC_REGULAR: + typenameTypeIdAndMod(pstate, rawc->typeName, &typid, &typmod); + + /* + * Use implicit FORMAT JSON for composite types (arrays and + * records) + */ + if (typeIsComposite(typid)) + rawc->coltype = JTC_FORMATTED; + else if (rawc->wrapper != JSW_NONE) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use WITH WRAPPER clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + else if (rawc->omit_quotes) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use OMIT QUOTES clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + + /* FALLTHROUGH */ + case JTC_EXISTS: + case JTC_FORMATTED: + { + Node *je; + CaseTestExpr *param = makeNode(CaseTestExpr); + + param->collation = InvalidOid; + param->typeId = cxt->contextItemTypid; + param->typeMod = -1; + + je = transformJsonTableColumn(rawc, (Node *) param, + NIL, errorOnError); + + colexpr = transformExpr(pstate, je, EXPR_KIND_FROM_FUNCTION); + assign_expr_collations(pstate, colexpr); + + typid = exprType(colexpr); + typmod = exprTypmod(colexpr); + break; + } + + case JTC_NESTED: + continue; + + default: + elog(ERROR, "unknown JSON_TABLE column type: %d", rawc->coltype); + break; + } + + tf->coltypes = lappend_oid(tf->coltypes, typid); + tf->coltypmods = lappend_int(tf->coltypmods, typmod); + tf->colcollations = lappend_oid(tf->colcollations, get_typcollation(typid)); + tf->colvalexprs = lappend(tf->colvalexprs, colexpr); + } +} + +/* + * Create transformed JSON_TABLE parent plan node by appending all non-nested + * columns to the TableFunc node and remembering their indices in the + * colvalexprs list. + */ +static JsonTableParent * +makeParentJsonTableNode(JsonTableParseContext *cxt, char *pathSpec, char *pathName, + List *columns) +{ + JsonTableParent *node = makeNode(JsonTableParent); + + node->path = makeNode(JsonTablePath); + node->path->value = makeConst(JSONPATHOID, -1, InvalidOid, -1, + DirectFunctionCall1(jsonpath_in, + CStringGetDatum(pathSpec)), + false, false); + if (pathName) + node->path->name = pstrdup(pathName); + + /* save start of column range */ + node->colMin = list_length(cxt->tablefunc->colvalexprs); + + appendJsonTableColumns(cxt, columns); + + /* save end of column range */ + node->colMax = list_length(cxt->tablefunc->colvalexprs) - 1; + + node->errorOnError = + cxt->table->on_error && + cxt->table->on_error->btype == JSON_BEHAVIOR_ERROR; + + return node; +} + +static JsonTableParent * +transformJsonTableColumns(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns, char *pathSpec, char **pathName, + int location) +{ + JsonTableParent *node; + JsonTablePlan *childPlan; + bool defaultPlan = !plan || plan->plan_type == JSTP_DEFAULT; + + if (!*pathName) + { + if (cxt->table->plan) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE expression"), + errdetail("JSON_TABLE columns must contain " + "explicit AS pathname specification if " + "explicit PLAN clause is used"), + parser_errposition(cxt->pstate, location))); + + *pathName = generateJsonTablePathName(cxt); + } + + if (defaultPlan) + childPlan = plan; + else + { + /* validate parent and child plans */ + JsonTablePlan *parentPlan; + + if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type != JSTPJ_INNER && + plan->join_type != JSTPJ_OUTER) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Expected INNER or OUTER JSON_TABLE plan node."), + parser_errposition(cxt->pstate, plan->location))); + + parentPlan = plan->plan1; + childPlan = plan->plan2; + + Assert(parentPlan->plan_type != JSTP_JOINED); + Assert(parentPlan->pathname); + } + else + { + parentPlan = plan; + childPlan = NULL; + } + + if (strcmp(parentPlan->pathname, *pathName)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name mismatch: expected %s but %s is given.", + *pathName, parentPlan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + validateJsonTableChildPlan(cxt->pstate, childPlan, columns); + } + + /* transform only non-nested columns */ + node = makeParentJsonTableNode(cxt, pathSpec, *pathName, columns); + + if (childPlan || defaultPlan) + { + /* transform recursively nested columns */ + node->child = transformJsonTableChildPlan(cxt, childPlan, columns); + if (node->child) + node->outerJoin = !plan || (plan->join_type & JSTPJ_OUTER); + /* else: default plan case, no children found */ + } + + return node; +} + +/* + * transformJsonTable - + * Transform a raw JsonTable into TableFunc. + * + * Transform the document-generating expression, the row-generating expression, + * the column-generating expressions, and the default value expressions. + */ +ParseNamespaceItem * +transformJsonTable(ParseState *pstate, JsonTable *jt) +{ + JsonTableParseContext cxt; + TableFunc *tf = makeNode(TableFunc); + JsonFuncExpr *jfe = makeNode(JsonFuncExpr); + JsonExpr *je; + JsonTablePlan *plan = jt->plan; + JsonCommon *jscommon; + char *rootPathName = jt->common->pathname; + char *rootPath; + bool is_lateral; + + cxt.pstate = pstate; + cxt.table = jt; + cxt.tablefunc = tf; + cxt.pathNames = NIL; + cxt.pathNameId = 0; + + if (rootPathName) + registerJsonTableColumn(&cxt, rootPathName); + + registerAllJsonTableColumns(&cxt, jt->columns); + +#if 0 /* XXX it' unclear from the standard whether + * root path name is mandatory or not */ + if (plan && plan->plan_type != JSTP_DEFAULT && !rootPathName) + { + /* Assign root path name and create corresponding plan node */ + JsonTablePlan *rootNode = makeNode(JsonTablePlan); + JsonTablePlan *rootPlan = (JsonTablePlan *) + makeJsonTableJoinedPlan(JSTPJ_OUTER, (Node *) rootNode, + (Node *) plan, jt->location); + + rootPathName = generateJsonTablePathName(&cxt); + + rootNode->plan_type = JSTP_SIMPLE; + rootNode->pathname = rootPathName; + + plan = rootPlan; + } +#endif + + jscommon = copyObject(jt->common); + jscommon->pathspec = makeStringConst(pstrdup("$"), -1); + + jfe->op = JSON_TABLE_OP; + jfe->common = jscommon; + jfe->on_error = jt->on_error; + jfe->location = jt->common->location; + + /* + * We make lateral_only names of this level visible, whether or not the + * RangeTableFunc is explicitly marked LATERAL. This is needed for SQL + * spec compliance and seems useful on convenience grounds for all + * functions in FROM. + * + * (LATERAL can't nest within a single pstate level, so we don't need + * save/restore logic here.) + */ + Assert(!pstate->p_lateral_active); + pstate->p_lateral_active = true; + + tf->functype = TFT_JSON_TABLE; + tf->docexpr = transformExpr(pstate, (Node *) jfe, EXPR_KIND_FROM_FUNCTION); + + cxt.contextItemTypid = exprType(tf->docexpr); + + if (!IsA(jt->common->pathspec, A_Const) || + castNode(A_Const, jt->common->pathspec)->val.node.type != T_String) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("only string constants supported in JSON_TABLE path specification"), + parser_errposition(pstate, + exprLocation(jt->common->pathspec)))); + + rootPath = castNode(A_Const, jt->common->pathspec)->val.sval.sval; + + tf->plan = (Node *) transformJsonTableColumns(&cxt, plan, jt->columns, + rootPath, &rootPathName, + jt->common->location); + + /* Also save a copy of the PASSING arguments in the TableFunc node. */ + je = (JsonExpr *) tf->docexpr; + tf->passingvalexprs = copyObject(je->passing_values); + + tf->ordinalitycol = -1; /* undefine ordinality column number */ + tf->location = jt->location; + + pstate->p_lateral_active = false; + + /* + * Mark the RTE as LATERAL if the user said LATERAL explicitly, or if + * there are any lateral cross-references in it. + */ + is_lateral = jt->lateral || contain_vars_of_level((Node *) tf, 0); + + return addRangeTableEntryForTableFunc(pstate, + tf, jt->alias, is_lateral, true); +} diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 41d60494b9..7da42c4772 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -2072,7 +2072,8 @@ addRangeTableEntryForTableFunc(ParseState *pstate, Assert(list_length(tf->coltypmods) == list_length(tf->colnames)); Assert(list_length(tf->colcollations) == list_length(tf->colnames)); - refname = alias ? alias->aliasname : pstrdup("xmltable"); + refname = alias ? alias->aliasname : + pstrdup(tf->functype == TFT_XMLTABLE ? "xmltable" : "json_table"); rte->rtekind = RTE_TABLEFUNC; rte->relid = InvalidOid; @@ -2095,7 +2096,7 @@ addRangeTableEntryForTableFunc(ParseState *pstate, ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), errmsg("%s function has %d columns available but %d columns specified", - "XMLTABLE", + tf->functype == TFT_XMLTABLE ? "XMLTABLE" : "JSON_TABLE", list_length(tf->colnames), numaliases))); rte->eref = eref; diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4f94fc69d6..6500e42485 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1992,6 +1992,9 @@ FigureColnameInternal(Node *node, char **name) case JSON_EXISTS_OP: *name = "json_exists"; return 2; + case JSON_TABLE_OP: + *name = "json_table"; + return 2; } break; default: diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index eded22e194..92d76d5cde 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -61,10 +61,12 @@ #include "catalog/pg_collation.h" #include "catalog/pg_type.h" +#include "executor/execExpr.h" #include "funcapi.h" #include "lib/stringinfo.h" #include "miscadmin.h" #include "nodes/miscnodes.h" +#include "nodes/nodeFuncs.h" #include "regex/regex.h" #include "utils/builtins.h" #include "utils/date.h" @@ -75,6 +77,8 @@ #include "utils/guc.h" #include "utils/json.h" #include "utils/jsonpath.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" #include "utils/timestamp.h" #include "utils/varlena.h" @@ -156,6 +160,61 @@ typedef struct JsonValueListIterator ListCell *next; } JsonValueListIterator; +/* Structures for JSON_TABLE execution */ + +typedef enum JsonTablePlanStateType +{ + JSON_TABLE_SCAN_STATE = 0, + JSON_TABLE_JOIN_STATE +} JsonTablePlanStateType; + +typedef struct JsonTablePlanState +{ + JsonTablePlanStateType type; + + struct JsonTablePlanState *parent; + struct JsonTablePlanState *nested; +} JsonTablePlanState; + +typedef struct JsonTableScanState +{ + JsonTablePlanState plan; + + MemoryContext mcxt; + JsonPath *path; + List *args; + JsonValueList found; + JsonValueListIterator iter; + Datum current; + int ordinal; + bool currentIsNull; + bool outerJoin; + bool errorOnError; + bool advanceNested; + bool reset; +} JsonTableScanState; + +typedef struct JsonTableJoinState +{ + JsonTablePlanState plan; + + JsonTablePlanState *left; + JsonTablePlanState *right; + bool cross; + bool advanceRight; +} JsonTableJoinState; + +/* random number to identify JsonTableExecContext */ +#define JSON_TABLE_EXEC_CONTEXT_MAGIC 418352867 + +typedef struct JsonTableExecContext +{ + int magic; + JsonTableScanState **colexprscans; + JsonTableScanState *root; + bool empty; +} JsonTableExecContext; + /* strict/lax flags is decomposed into four [un]wrap/error flags */ #define jspStrictAbsenseOfErrors(cxt) (!(cxt)->laxMode) #define jspAutoUnwrap(cxt) ((cxt)->laxMode) @@ -248,6 +307,7 @@ static JsonPathExecResult getArrayIndex(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbValue *jb, int32 *index); static JsonBaseObjectInfo setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id); +static void JsonValueListClear(JsonValueList *jvl); static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv); static int JsonValueListLength(const JsonValueList *jvl); static bool JsonValueListIsEmpty(JsonValueList *jvl); @@ -265,6 +325,14 @@ static JsonbValue *wrapItemsInArray(const JsonValueList *items); static int compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, bool useTz, bool *cast_error); + +static JsonTablePlanState *JsonTableInitPlanState(JsonTableExecContext *cxt, + Node *plan, + JsonTablePlanState *parent); +static bool JsonTablePlanNextRow(JsonTablePlanState *state); +static bool JsonTableScanNextRow(JsonTableScanState *scan); + + /****************** User interface to JsonPath executor ********************/ /* @@ -2518,6 +2586,13 @@ setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id) return baseObject; } +static void +JsonValueListClear(JsonValueList *jvl) +{ + jvl->singleton = NULL; + jvl->list = NULL; +} + static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv) { @@ -3123,3 +3198,471 @@ JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); } } + +/************************ JSON_TABLE functions ***************************/ + +/* + * Returns private data from executor state. Ensure validity by check with + * MAGIC number. + */ +static inline JsonTableExecContext * +GetJsonTableExecContext(TableFuncScanState *state, const char *fname) +{ + JsonTableExecContext *result; + + if (!IsA(state, TableFuncScanState)) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + result = (JsonTableExecContext *) state->opaque; + if (result->magic != JSON_TABLE_EXEC_CONTEXT_MAGIC) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + + return result; +} + +/* Recursively initialize JSON_TABLE scan / join state */ +static JsonTableJoinState * +JsonTableInitJoinState(JsonTableExecContext *cxt, JsonTableSibling *plan, + JsonTablePlanState *parent) +{ + JsonTableJoinState *join = palloc0(sizeof(*join)); + + join->plan.type = JSON_TABLE_JOIN_STATE; + /* parent and nested not set. */ + + join->cross = plan->cross; + join->left = JsonTableInitPlanState(cxt, plan->larg, parent); + join->right = JsonTableInitPlanState(cxt, plan->rarg, parent); + + return join; +} + +static JsonTableScanState * +JsonTableInitScanState(JsonTableExecContext *cxt, JsonTableParent *plan, + JsonTablePlanState *parent, + List *args, MemoryContext mcxt) +{ + JsonTableScanState *scan = palloc0(sizeof(*scan)); + int i; + + scan->plan.type = JSON_TABLE_SCAN_STATE; + scan->plan.parent = parent; + + scan->outerJoin = plan->outerJoin; + scan->errorOnError = plan->errorOnError; + scan->path = DatumGetJsonPathP(plan->path->value->constvalue); + scan->args = args; + scan->mcxt = AllocSetContextCreate(mcxt, "JsonTableExecContext", + ALLOCSET_DEFAULT_SIZES); + + /* + * Set after settings scan->args and scan->mcxt, because the recursive call + * wants to use those values. + */ + scan->plan.nested = plan->child ? + JsonTableInitPlanState(cxt, plan->child, (JsonTablePlanState *) scan) : + NULL; + + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + + for (i = plan->colMin; i <= plan->colMax; i++) + cxt->colexprscans[i] = scan; + + return scan; +} + +/* Recursively initialize JSON_TABLE scan state */ +static JsonTablePlanState * +JsonTableInitPlanState(JsonTableExecContext *cxt, Node *plan, + JsonTablePlanState *parent) +{ + JsonTablePlanState *state; + + if (IsA(plan, JsonTableSibling)) + { + JsonTableSibling *join = castNode(JsonTableSibling, plan); + + state = (JsonTablePlanState *) + JsonTableInitJoinState(cxt, join, parent); + } + else + { + JsonTableParent *scan = castNode(JsonTableParent, plan); + JsonTableScanState *parent_scan = (JsonTableScanState *) parent; + + Assert(parent_scan); + state = (JsonTablePlanState *) + JsonTableInitScanState(cxt, scan, parent, parent_scan->args, + parent_scan->mcxt); + } + + return state; +} + +/* + * JsonTableInitOpaque + * Fill in TableFuncScanState->opaque for JsonTable processor + */ +static void +JsonTableInitOpaque(TableFuncScanState *state, int natts) +{ + JsonTableExecContext *cxt; + PlanState *ps = &state->ss.ps; + TableFuncScan *tfs = castNode(TableFuncScan, ps->plan); + TableFunc *tf = tfs->tablefunc; + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + JsonExpr *je = castNode(JsonExpr, tf->docexpr); + List *args = NIL; + + cxt = palloc0(sizeof(JsonTableExecContext)); + cxt->magic = JSON_TABLE_EXEC_CONTEXT_MAGIC; + + if (state->passingvalexprs) + { + ListCell *exprlc; + ListCell *namelc; + + Assert(list_length(state->passingvalexprs) == + list_length(je->passing_names)); + forboth(exprlc, state->passingvalexprs, + namelc, je->passing_names) + { + ExprState *state = lfirst_node(ExprState, exprlc); + String *name = lfirst_node(String, namelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(name->sval); + var->typid = exprType((Node *) state->expr); + var->typmod = exprTypmod((Node *) state->expr); + + /* + * Evaluate the expression and save the value to be returned by + * GetJsonPathVar(). + */ + var->value = ExecEvalExpr(state, ps->ps_ExprContext, + &var->isnull); + + args = lappend(args, var); + } + } + + cxt->colexprscans = palloc(sizeof(JsonTableScanState *) * + list_length(tf->colvalexprs)); + + cxt->root = JsonTableInitScanState(cxt, root, NULL, args, + CurrentMemoryContext); + state->opaque = cxt; +} + +/* Reset scan iterator to the beginning of the item list */ +static void +JsonTableRescan(JsonTableScanState *scan) +{ + JsonValueListInitIterator(&scan->found, &scan->iter); + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + scan->advanceNested = false; + scan->ordinal = 0; +} + +/* Reset context item of a scan, execute JSON path and reset a scan */ +static void +JsonTableResetContextItem(JsonTableScanState *scan, Datum item) +{ + MemoryContext oldcxt; + JsonPathExecResult res; + Jsonb *js = (Jsonb *) DatumGetJsonbP(item); + + JsonValueListClear(&scan->found); + + MemoryContextResetOnly(scan->mcxt); + + oldcxt = MemoryContextSwitchTo(scan->mcxt); + + res = executeJsonPath(scan->path, scan->args, GetJsonPathVar, js, + scan->errorOnError, &scan->found, + false /* FIXME */ ); + + MemoryContextSwitchTo(oldcxt); + + if (jperIsError(res)) + { + Assert(!scan->errorOnError); + JsonValueListClear(&scan->found); /* EMPTY ON ERROR case */ + } + + JsonTableRescan(scan); +} + +/* + * JsonTableSetDocument + * Install the input document + */ +static void +JsonTableSetDocument(TableFuncScanState *state, Datum value) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableSetDocument"); + + JsonTableResetContextItem(cxt->root, value); +} + +/* Recursively reset scan and its child nodes */ +static void +JsonTableRescanRecursive(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTableRescanRecursive(join->left); + JsonTableRescanRecursive(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan = (JsonTableScanState *) state; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + JsonTableRescan(scan); + if (scan->plan.nested) + JsonTableRescanRecursive(scan->plan.nested); + } +} + +/* + * Fetch next row from a cross/union joined scan. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTablePlanNextRow(JsonTablePlanState *state) +{ + JsonTableJoinState *join; + + if (state->type == JSON_TABLE_SCAN_STATE) + return JsonTableScanNextRow((JsonTableScanState *) state); + + join = (JsonTableJoinState *) state; + if (join->advanceRight) + { + /* fetch next inner row */ + if (JsonTablePlanNextRow(join->right)) + return true; + + /* inner rows are exhausted */ + if (join->cross) + join->advanceRight = false; /* next outer row */ + else + return false; /* end of scan */ + } + + while (!join->advanceRight) + { + /* fetch next outer row */ + bool left = JsonTablePlanNextRow(join->left); + + if (join->cross) + { + if (!left) + return false; /* end of scan */ + + JsonTableRescanRecursive(join->right); + + if (!JsonTablePlanNextRow(join->right)) + continue; /* next outer row */ + + join->advanceRight = true; /* next inner row */ + } + else if (!left) + { + if (!JsonTablePlanNextRow(join->right)) + return false; /* end of scan */ + + join->advanceRight = true; /* next inner row */ + } + + break; + } + + return true; +} + +/* Recursively set 'reset' flag of scan and its child nodes */ +static void +JsonTablePlanReset(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTablePlanReset(join->left); + JsonTablePlanReset(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + scan = (JsonTableScanState *) state; + scan->reset = true; + scan->advanceNested = false; + + if (scan->plan.nested) + JsonTablePlanReset(scan->plan.nested); + } +} + +/* + * Fetch next row from a simple scan with outer/inner joined nested subscans. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTableScanNextRow(JsonTableScanState *scan) +{ + /* reset context item if requested */ + if (scan->reset) + { + JsonTableScanState *parent_scan = + (JsonTableScanState *) scan->plan.parent; + + Assert(parent_scan && !parent_scan->currentIsNull); + JsonTableResetContextItem(scan, parent_scan->current); + scan->reset = false; + } + + if (scan->advanceNested) + { + /* fetch next nested row */ + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested) + return true; + } + + for (;;) + { + /* fetch next row */ + JsonbValue *jbv = JsonValueListNext(&scan->found, &scan->iter); + MemoryContext oldcxt; + + if (!jbv) + { + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + return false; /* end of scan */ + } + + /* set current row item */ + oldcxt = MemoryContextSwitchTo(scan->mcxt); + scan->current = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + scan->currentIsNull = false; + MemoryContextSwitchTo(oldcxt); + + scan->ordinal++; + + if (!scan->plan.nested) + break; + + JsonTablePlanReset(scan->plan.nested); + + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested || scan->outerJoin) + break; + } + + return true; +} + +/* + * JsonTableFetchRow + * Prepare the next "current" tuple for upcoming GetValue calls. + * Returns FALSE if the row-filter expression returned no more rows. + */ +static bool +JsonTableFetchRow(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableFetchRow"); + + if (cxt->empty) + return false; + + return JsonTableScanNextRow(cxt->root); +} + +/* + * JsonTableGetValue + * Return the value for column number 'colnum' for the current row. + * + * This leaks memory, so be sure to reset often the context in which it's + * called. + */ +static Datum +JsonTableGetValue(TableFuncScanState *state, int colnum, + Oid typid, int32 typmod, bool *isnull) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableGetValue"); + ExprContext *econtext = state->ss.ps.ps_ExprContext; + ExprState *estate = list_nth(state->colvalexprs, colnum); + JsonTableScanState *scan = cxt->colexprscans[colnum]; + Datum result; + + if (scan->currentIsNull) /* NULL from outer/union join */ + { + result = (Datum) 0; + *isnull = true; + } + else if (estate) /* regular column */ + { + Datum saved_caseValue = econtext->caseValue_datum; + bool saved_caseIsNull = econtext->caseValue_isNull; + + /* Pass the value for CaseTestExpr that may be present in colexpr */ + econtext->caseValue_datum = scan->current; + econtext->caseValue_isNull = false; + + result = ExecEvalExpr(estate, econtext, isnull); + + econtext->caseValue_datum = saved_caseValue; + econtext->caseValue_isNull = saved_caseIsNull; + } + else + { + result = Int32GetDatum(scan->ordinal); /* ordinality column */ + *isnull = false; + } + + return result; +} + +/* + * JsonTableDestroyOpaque + */ +static void +JsonTableDestroyOpaque(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableDestroyOpaque"); + + /* not valid anymore */ + cxt->magic = 0; + + state->opaque = NULL; +} + +const TableFuncRoutine JsonbTableRoutine = +{ + JsonTableInitOpaque, + JsonTableSetDocument, + NULL, + NULL, + NULL, + JsonTableFetchRow, + JsonTableGetValue, + JsonTableDestroyOpaque +}; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d1ec418ccf..e89d1e03d6 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -522,6 +522,8 @@ static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); static void get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit); +static void get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8606,7 +8608,8 @@ get_json_behavior(JsonBehavior *behavior, deparse_context *context, /* * get_json_expr_options * - * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS and + * JSON_TABLE columns. */ static void get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, @@ -9854,6 +9857,9 @@ get_rule_expr(Node *node, deparse_context *context, case JSON_EXISTS_OP: appendStringInfoString(buf, "JSON_EXISTS("); break; + default: + elog(ERROR, "unexpected JsonExpr type: %d", jexpr->op); + break; } get_rule_expr(jexpr->formatted_expr, context, showimplicit); @@ -11207,16 +11213,14 @@ get_sublink_expr(SubLink *sublink, deparse_context *context) /* ---------- - * get_tablefunc - Parse back a table function + * get_xmltable - Parse back a XMLTABLE function * ---------- */ static void -get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +get_xmltable(TableFunc *tf, deparse_context *context, bool showimplicit) { StringInfo buf = context->buf; - /* XMLTABLE is the only existing implementation. */ - appendStringInfoString(buf, "XMLTABLE("); if (tf->ns_uris != NIL) @@ -11307,6 +11311,271 @@ get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) appendStringInfoChar(buf, ')'); } +/* + * get_json_nested_columns - Parse back nested JSON_TABLE columns + */ +static void +get_json_table_nested_columns(TableFunc *tf, Node *node, + deparse_context *context, bool showimplicit, + bool needcomma) +{ + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_nested_columns(tf, n->larg, context, showimplicit, + needcomma); + get_json_table_nested_columns(tf, n->rarg, context, showimplicit, true); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + if (needcomma) + appendStringInfoChar(context->buf, ','); + + appendStringInfoChar(context->buf, ' '); + appendContextKeyword(context, "NESTED PATH ", 0, 0, 0); + get_const_expr(n->path->value, context, -1); + appendStringInfo(context->buf, " AS %s", quote_identifier(n->path->name)); + get_json_table_columns(tf, n, context, showimplicit); + } +} + +/* + * get_json_table_plan - Parse back a JSON_TABLE plan + */ +static void +get_json_table_plan(TableFunc *tf, Node *node, deparse_context *context, + bool parenthesize) +{ + if (parenthesize) + appendStringInfoChar(context->buf, '('); + + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_plan(tf, n->larg, context, + IsA(n->larg, JsonTableSibling) || + castNode(JsonTableParent, n->larg)->child); + + appendStringInfoString(context->buf, n->cross ? " CROSS " : " UNION "); + + get_json_table_plan(tf, n->rarg, context, + IsA(n->rarg, JsonTableSibling) || + castNode(JsonTableParent, n->rarg)->child); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + appendStringInfoString(context->buf, quote_identifier(n->path->name)); + + if (n->child) + { + appendStringInfoString(context->buf, + n->outerJoin ? " OUTER " : " INNER "); + get_json_table_plan(tf, n->child, context, + IsA(n->child, JsonTableSibling)); + } + } + + if (parenthesize) + appendStringInfoChar(context->buf, ')'); +} + +/* + * get_json_table_columns - Parse back JSON_TABLE columns + */ +static void +get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + ListCell *lc_colname; + ListCell *lc_coltype; + ListCell *lc_coltypmod; + ListCell *lc_colvalexpr; + int colnum = 0; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "COLUMNS (", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forfour(lc_colname, tf->colnames, + lc_coltype, tf->coltypes, + lc_coltypmod, tf->coltypmods, + lc_colvalexpr, tf->colvalexprs) + { + char *colname = strVal(lfirst(lc_colname)); + JsonExpr *colexpr; + Oid typid; + int32 typmod; + bool ordinality; + JsonBehaviorType default_behavior; + + typid = lfirst_oid(lc_coltype); + typmod = lfirst_int(lc_coltypmod); + colexpr = castNode(JsonExpr, lfirst(lc_colvalexpr)); + + if (colnum < node->colMin) + { + colnum++; + continue; + } + + if (colnum > node->colMax) + break; + + if (colnum > node->colMin) + appendStringInfoString(buf, ", "); + + colnum++; + + ordinality = !colexpr; + + appendContextKeyword(context, "", 0, 0, 0); + + appendStringInfo(buf, "%s %s", quote_identifier(colname), + ordinality ? "FOR ORDINALITY" : + format_type_with_typemod(typid, typmod)); + if (ordinality) + continue; + + if (colexpr->op == JSON_EXISTS_OP) + { + appendStringInfoString(buf, " EXISTS"); + default_behavior = JSON_BEHAVIOR_FALSE; + } + else + { + if (colexpr->op == JSON_QUERY_OP) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(typid, &typcategory, &typispreferred); + + if (typcategory == TYPCATEGORY_STRING) + appendStringInfoString(buf, + colexpr->format->format_type == JS_FORMAT_JSONB ? + " FORMAT JSONB" : " FORMAT JSON"); + } + + default_behavior = JSON_BEHAVIOR_NULL; + } + + if (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR) + default_behavior = JSON_BEHAVIOR_ERROR; + + appendStringInfoString(buf, " PATH "); + + get_json_path_spec(colexpr->path_spec, context, showimplicit); + + get_json_expr_options(colexpr, context, default_behavior); + } + + if (node->child) + get_json_table_nested_columns(tf, node->child, context, showimplicit, + node->colMax >= node->colMin); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_json_table - Parse back a JSON_TABLE function + * ---------- + */ +static void +get_json_table(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + + appendStringInfoString(buf, "JSON_TABLE("); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_const_expr(root->path->value, context, -1); + + appendStringInfo(buf, " AS %s", quote_identifier(root->path->name)); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PASSING ", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr((Node *) lfirst(lc2), context, false); + appendStringInfo(buf, " AS %s", + quote_identifier((lfirst_node(String, lc1))->sval) + ); + } + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + } + + get_json_table_columns(tf, root, context, showimplicit); + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PLAN ", 0, 0, 0); + get_json_table_plan(tf, (Node *) root, context, true); + + if (jexpr->on_error->btype != JSON_BEHAVIOR_EMPTY) + get_json_behavior(jexpr->on_error, context, "ERROR"); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_tablefunc - Parse back a table function + * ---------- + */ +static void +get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + /* XMLTABLE and JSON_TABLE are the only existing implementations. */ + + if (tf->functype == TFT_XMLTABLE) + get_xmltable(tf, context, showimplicit); + else if (tf->functype == TFT_JSON_TABLE) + get_json_table(tf, context, showimplicit); +} + /* ---------- * get_from_clause - Parse back a FROM clause * diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 584bf7001a..91453681e3 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1879,6 +1879,8 @@ typedef struct TableFuncScanState ExprState *rowexpr; /* state for row-generating expression */ List *colexprs; /* state for column-generating expression */ List *coldefexprs; /* state for column default expressions */ + List *colvalexprs; /* state for column value expression */ + List *passingvalexprs; /* state for PASSING argument expression */ List *ns_names; /* same as TableFunc.ns_names */ List *ns_uris; /* list of states of namespace URI exprs */ Bitmapset *notnulls; /* nullability flag for each output column */ diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 5e149b0266..d8466a2699 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -112,6 +112,8 @@ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *expr, JsonFormat *format); extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); +extern Node *makeJsonTableJoinedPlan(JsonTablePlanJoinType type, + Node *plan1, Node *plan2, int location); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 7d63a37d5f..b15f71cc92 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1733,6 +1733,19 @@ typedef enum JsonQuotes */ typedef char *JsonPathSpec; +/* + * JsonTableColumnType - + * enumeration of JSON_TABLE column types + */ +typedef enum JsonTableColumnType +{ + JTC_FOR_ORDINALITY, + JTC_REGULAR, + JTC_EXISTS, + JTC_FORMATTED, + JTC_NESTED, +} JsonTableColumnType; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1786,6 +1799,83 @@ typedef struct JsonFuncExpr int location; /* token location, or -1 if unknown */ } JsonFuncExpr; +/* + * JsonTableColumn - + * untransformed representation of JSON_TABLE column + */ +typedef struct JsonTableColumn +{ + NodeTag type; + JsonTableColumnType coltype; /* column type */ + char *name; /* column name */ + TypeName *typeName; /* column type name */ + char *pathspec; /* path specification, if any */ + char *pathname; /* path name, if any */ + JsonFormat *format; /* JSON format clause, if specified */ + JsonWrapper wrapper; /* WRAPPER behavior for formatted columns */ + bool omit_quotes; /* omit or keep quotes on scalar strings? */ + List *columns; /* nested columns */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + int location; /* token location, or -1 if unknown */ +} JsonTableColumn; + +/* + * JsonTablePlanType - + * flags for JSON_TABLE plan node types representation + */ +typedef enum JsonTablePlanType +{ + JSTP_DEFAULT, + JSTP_SIMPLE, + JSTP_JOINED, +} JsonTablePlanType; + +/* + * JsonTablePlanJoinType - + * flags for JSON_TABLE join types representation + */ +typedef enum JsonTablePlanJoinType +{ + JSTPJ_INNER = 0x01, + JSTPJ_OUTER = 0x02, + JSTPJ_CROSS = 0x04, + JSTPJ_UNION = 0x08, +} JsonTablePlanJoinType; + +typedef struct JsonTablePlan JsonTablePlan; + +/* + * JsonTablePlan - + * untransformed representation of JSON_TABLE plan node + */ +struct JsonTablePlan +{ + NodeTag type; + JsonTablePlanType plan_type; /* plan type */ + JsonTablePlanJoinType join_type; /* join type (for joined plan only) */ + JsonTablePlan *plan1; /* first joined plan */ + JsonTablePlan *plan2; /* second joined plan */ + char *pathname; /* path name (for simple plan only) */ + int location; /* token location, or -1 if unknown */ +}; + +/* + * JsonTable - + * untransformed representation of JSON_TABLE + */ +typedef struct JsonTable +{ + NodeTag type; + JsonCommon *common; /* common JSON path syntax fields */ + List *columns; /* list of JsonTableColumn */ + JsonTablePlan *plan; /* join plan, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + Alias *alias; /* table alias in FROM clause */ + bool lateral; /* does it have LATERAL prefix? */ + int location; /* token location, or -1 if unknown */ +} JsonTable; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 62345a939e..a20880f4bc 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -86,8 +86,14 @@ typedef struct RangeVar int location; } RangeVar; +typedef enum TableFuncType +{ + TFT_XMLTABLE, + TFT_JSON_TABLE +} TableFuncType; + /* - * TableFunc - node for a table function, such as XMLTABLE. + * TableFunc - node for a table function, such as XMLTABLE or JSON_TABLE. * * Entries in the ns_names list are either String nodes containing * literal namespace names, or NULL pointers to represent DEFAULT. @@ -95,6 +101,8 @@ typedef struct RangeVar typedef struct TableFunc { NodeTag type; + /* XMLTABLE or JSON_TABLE */ + TableFuncType functype; /* list of namespace URI expressions */ List *ns_uris pg_node_attr(query_jumble_ignore); /* list of namespace names or NULL */ @@ -115,8 +123,14 @@ typedef struct TableFunc List *colexprs; /* list of column default expressions */ List *coldefexprs pg_node_attr(query_jumble_ignore); + /* list of column value expressions */ + List *colvalexprs pg_node_attr(query_jumble_ignore); + /* list of PASSING argument expressions */ + List *passingvalexprs pg_node_attr(query_jumble_ignore); /* nullability flag for each output column */ Bitmapset *notnulls pg_node_attr(query_jumble_ignore); + /* JSON_TABLE plan */ + Node *plan pg_node_attr(query_jumble_ignore); /* counts from 0; -1 if none specified */ int ordinalitycol pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ @@ -1550,7 +1564,8 @@ typedef enum JsonExprOp { JSON_VALUE_OP, /* JSON_VALUE() */ JSON_QUERY_OP, /* JSON_QUERY() */ - JSON_EXISTS_OP /* JSON_EXISTS() */ + JSON_EXISTS_OP, /* JSON_EXISTS() */ + JSON_TABLE_OP /* JSON_TABLE() */ } JsonExprOp; /* @@ -1768,6 +1783,48 @@ typedef struct JsonExpr int location; /* token location, or -1 if unknown */ } JsonExpr; +/* + * JsonTablePath + * A JSON path expression to be computed as part of evaluating + * a JSON_TABLE plan node + */ +typedef struct JsonTablePath +{ + NodeTag type; + + Const *value; + char *name; +} JsonTablePath; + +/* + * JsonTableParent - + * transformed representation of parent JSON_TABLE plan node + */ +typedef struct JsonTableParent +{ + NodeTag type; + JsonTablePath *path; + Node *child; /* nested columns, if any */ + bool outerJoin; /* outer or inner join for nested columns? */ + int colMin; /* min column index in the resulting column + * list */ + int colMax; /* max column index in the resulting column + * list */ + bool errorOnError; /* ERROR/EMPTY ON ERROR behavior */ +} JsonTableParent; + +/* + * JsonTableSibling - + * transformed representation of joined sibling JSON_TABLE plan node + */ +typedef struct JsonTableSibling +{ + NodeTag type; + Node *larg; /* left join node */ + Node *rarg; /* right join node */ + bool cross; /* cross or union join? */ +} JsonTableSibling; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0954d9fc7b..f88a6c9ac6 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -242,6 +242,7 @@ PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_table", JSON_TABLE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) @@ -284,6 +285,7 @@ PG_KEYWORD("names", NAMES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("national", NATIONAL, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("natural", NATURAL, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("nchar", NCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("nested", NESTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("new", NEW, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("next", NEXT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("nfc", NFC, UNRESERVED_KEYWORD, BARE_LABEL) @@ -334,7 +336,9 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h index 8e26482db4..7152a8ef48 100644 --- a/src/include/parser/parse_clause.h +++ b/src/include/parser/parse_clause.h @@ -51,4 +51,7 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle, extern Index assignSortGroupRef(TargetEntry *tle, List *tlist); extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList); +/* functions in parse_jsontable.c */ +extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt); + #endif /* PARSE_CLAUSE_H */ diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index 4dae78a98c..b7ada77cb1 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -15,6 +15,7 @@ #define JSONPATH_H #include "fmgr.h" +#include "executor/tablefunc.h" #include "nodes/pg_list.h" #include "nodes/primnodes.h" #include "utils/jsonb.h" @@ -288,4 +289,6 @@ extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars); +extern PGDLLIMPORT const TableFuncRoutine JsonbTableRoutine; + #endif diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out index 8b87580752..995f267404 100644 --- a/src/test/regress/expected/json_sqljson.out +++ b/src/test/regress/expected/json_sqljson.out @@ -16,3 +16,9 @@ ERROR: JSON_QUERY() is not yet implemented for the json type LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); ^ HINT: Try casting the argument to jsonb +-- JSON_TABLE +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); +ERROR: JSON_TABLE() is not yet implemented for the json type +LINE 1: SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo ... + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out index 22f8679917..4323ed6b35 100644 --- a/src/test/regress/expected/jsonb_sqljson.out +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -1040,3 +1040,1072 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.dateti ERROR: functions in index expression must be marked IMMUTABLE CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; +-- JSON_TABLE +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); +ERROR: syntax error at or near "(" +LINE 1: SELECT JSON_TABLE('[]', '$'); + ^ +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); +ERROR: syntax error at or near ")" +LINE 1: SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + ^ +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); +ERROR: JSON_TABLE function has 1 columns available but 2 columns specified +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + foo +----- +(0 rows) + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + item | foo +------+----- + 123 | +(1 row) + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + js | id | id2 | int | text | char(4) | bool | numeric | domain | js | jb | jst | jsc | jsv | jsb | jsbq | aaa | aaa1 | exists1 | exists2 | exists3 | exists4 | js2 | jsb2w | jsb2q | ia | ta | jba +---------------------------------------------------------------------------------------+----+-----+-----+---------+---------+------+---------+---------+--------------+--------------+--------------+------+------+--------------+--------------+-----+------+---------+---------+---------+---------+--------------+----------------+--------------+----+----+----- + 1 | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [] | | | | | | | | | | | | | | | | | | | | | | | | | | | + {} | 1 | 1 | | | | | | | {} | {} | {} | {} | {} | {} | {} | | | f | 0 | | false | {} | [{}] | {} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 2 | 2 | 1 | 1.23 | 1.23 | | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | | | f | 0 | | false | 1.23 | [1.23] | 1.23 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 3 | 3 | 2 | 2 | 2 | | 2 | 2 | "2" | "2" | "2" | "2" | "2" | "2" | 2 | | | f | 0 | | false | "2" | ["2"] | 2 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 4 | 4 | | aaaaaaa | aaaa | | | aaaaaaa | "aaaaaaa" | "aaaaaaa" | "aaaaaaa" | "aaa | "aaa | "aaaaaaa" | | | | f | 0 | | false | "aaaaaaa" | ["aaaaaaa"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 5 | 5 | | foo | foo | | | | "foo" | "foo" | "foo" | "foo | "foo | "foo" | | | | f | 0 | | false | "foo" | ["foo"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 6 | 6 | | | | | | | null | null | null | null | null | null | null | | | f | 0 | | false | null | [null] | null | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 7 | 7 | 0 | false | fals | f | | false | false | false | false | fals | fals | false | false | | | f | 0 | | false | false | [false] | false | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 8 | 8 | 1 | true | true | t | | true | true | true | true | true | true | true | true | | | f | 0 | | false | true | [true] | true | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 9 | 9 | | | | | | | {"aaa": 123} | {"aaa": 123} | {"aaa": 123} | {"aa | {"aa | {"aaa": 123} | {"aaa": 123} | 123 | 123 | t | 1 | 1 | true | {"aaa": 123} | [{"aaa": 123}] | {"aaa": 123} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 10 | 10 | | [1,2] | [1,2 | | | [1,2] | "[1,2]" | "[1,2]" | "[1,2]" | "[1, | "[1, | "[1,2]" | [1, 2] | | | f | 0 | | false | "[1,2]" | ["[1,2]"] | [1, 2] | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 11 | 11 | | "str" | "str | | | "str" | "\"str\"" | "\"str\"" | "\"str\"" | "\"s | "\"s | "\"str\"" | "str" | | | f | 0 | | false | "\"str\"" | ["\"str\""] | "str" | | | +(14 rows) + +-- JSON_TABLE: Test backward parsing +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); +\sv jsonb_table_view +CREATE OR REPLACE VIEW public.jsonb_table_view AS + SELECT id, + id2, + "int", + text, + "char(4)", + bool, + "numeric", + domain, + js, + jb, + jst, + jsc, + jsv, + jsb, + jsbq, + aaa, + aaa1, + exists1, + exists2, + exists3, + js2, + jsb2w, + jsb2q, + ia, + ta, + jba, + a1, + b1, + a11, + a21, + a22 + FROM JSON_TABLE( + 'null'::jsonb, '$[*]' AS json_table_path_1 + PASSING + 1 + 2 AS a, + '"foo"'::json AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, + "int" integer PATH '$', + text text PATH '$', + "char(4)" character(4) PATH '$', + bool boolean PATH '$', + "numeric" numeric PATH '$', + domain jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc character(4) FORMAT JSON PATH '$', + jsv character varying(4) FORMAT JSON PATH '$', + jsb jsonb PATH '$', + jsbq jsonb PATH '$' OMIT QUOTES, + aaa integer PATH '$."aaa"', + aaa1 integer PATH '$."aaa"', + exists1 boolean EXISTS PATH '$."aaa"', + exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia integer[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 + COLUMNS ( + a1 integer PATH '$."a1"', + b1 text PATH '$."b1"', + NESTED PATH '$[*]' AS "p1 1" + COLUMNS ( + a11 text PATH '$."a11"' + ) + ), + NESTED PATH '$[2]' AS p2 + COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" + COLUMNS ( + a21 text PATH '$."a21"' + ), + NESTED PATH '$[*]' AS p22 + COLUMNS ( + a22 text PATH '$."a22"' + ) + ) + ) + PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22)))) + ) +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Table Function Scan on "json_table" + Output: "json_table".id, "json_table".id2, "json_table"."int", "json_table".text, "json_table"."char(4)", "json_table".bool, "json_table"."numeric", "json_table".domain, "json_table".js, "json_table".jb, "json_table".jst, "json_table".jsc, "json_table".jsv, "json_table".jsb, "json_table".jsbq, "json_table".aaa, "json_table".aaa1, "json_table".exists1, "json_table".exists2, "json_table".exists3, "json_table".js2, "json_table".jsb2w, "json_table".jsb2q, "json_table".ia, "json_table".ta, "json_table".jba, "json_table".a1, "json_table".b1, "json_table".a11, "json_table".a21, "json_table".a22 + Table Function Call: JSON_TABLE('null'::jsonb, '$[*]' AS json_table_path_1 PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (id FOR ORDINALITY, id2 FOR ORDINALITY, "int" integer PATH '$', text text PATH '$', "char(4)" character(4) PATH '$', bool boolean PATH '$', "numeric" numeric PATH '$', domain jsonb_test_domain PATH '$', js json PATH '$', jb jsonb PATH '$', jst text FORMAT JSON PATH '$', jsc character(4) FORMAT JSON PATH '$', jsv character varying(4) FORMAT JSON PATH '$', jsb jsonb PATH '$', jsbq jsonb PATH '$' OMIT QUOTES, aaa integer PATH '$."aaa"', aaa1 integer PATH '$."aaa"', exists1 boolean EXISTS PATH '$."aaa"', exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, js2 json PATH '$', jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, jsb2q jsonb PATH '$' OMIT QUOTES, ia integer[] PATH '$', ta text[] PATH '$', jba jsonb[] PATH '$', NESTED PATH '$[1]' AS p1 COLUMNS (a1 integer PATH '$."a1"', b1 text PATH '$."b1"', NESTED PATH '$[*]' AS "p1 1" COLUMNS (a11 text PATH '$."a11"')), NESTED PATH '$[2]' AS p2 COLUMNS ( NESTED PATH '$[*]' AS "p2:1" COLUMNS (a21 text PATH '$."a21"'), NESTED PATH '$[*]' AS p22 COLUMNS (a22 text PATH '$."a22"'))) PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22))))) +(3 rows) + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + js | a +-------+--- + 1 | 1 + "err" | +(2 rows) + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; + a +--- + +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: jsonpath member accessor can only be applied to an object +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: no SQL/JSON item +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 1 +(1 row) + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); + a +--- + 0 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to smallint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to bigint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to real +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 E... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); + a +----- + fal +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to json +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to jsonb +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EX... + ^ +-- JSON_TABLE: nested paths and plans +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb '[]', '$' -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 4: NESTED PATH '$' COLUMNS ( -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: b +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +-- JSON_TABLE: plan validation +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p1) + ^ +DETAIL: Path name mismatch: expected p0 but p1 is given. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 UNION p1 UNION p11) + ^ +DETAIL: Expected INNER or OUTER JSON_TABLE plan node. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 8: NESTED PATH '$' AS p2 COLUMNS ( + ^ +DETAIL: Plan node for nested path p2 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 5: NESTED PATH '$' AS p11 COLUMNS ( foo int ), + ^ +DETAIL: Plan node for nested path p11 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) + ^ +DETAIL: Plan node contains some extra or duplicate sibling nodes. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 6: NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ^ +DETAIL: Plan node for nested path p12 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 9: NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ^ +DETAIL: Plan node for nested path p21 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + bar | foo | baz +-----+-----+----- +(0 rows) + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb 'null', 'strict $[*]' -- without root path name + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- JSON_TABLE: plan execution +CREATE TEMP TABLE jsonb_table_test (js jsonb); +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + n | a | c | b +---+----+----+--- + 1 | 1 | | + 2 | 2 | 10 | + 2 | 2 | | + 2 | 2 | 20 | + 2 | 2 | | 1 + 2 | 2 | | 2 + 2 | 2 | | 3 + 3 | 3 | | 1 + 3 | 3 | | 2 + 4 | -1 | | 1 + 4 | -1 | | 2 +(11 rows) + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + n | a | b | b1 | c | c1 | b +---+---+--------------+-----+------+----+----- + 1 | 1 | [1, 10] | 1 | 1 | | 101 + 1 | 1 | [1, 10] | 1 | null | | 101 + 1 | 1 | [1, 10] | 1 | 2 | | 101 + 1 | 1 | [1, 10] | 10 | 1 | | 110 + 1 | 1 | [1, 10] | 10 | null | | 110 + 1 | 1 | [1, 10] | 10 | 2 | | 110 + 1 | 1 | [2] | 2 | 1 | | 102 + 1 | 1 | [2] | 2 | null | | 102 + 1 | 1 | [2] | 2 | 2 | | 102 + 1 | 1 | [3, 30, 300] | 3 | 1 | | 103 + 1 | 1 | [3, 30, 300] | 3 | null | | 103 + 1 | 1 | [3, 30, 300] | 3 | 2 | | 103 + 1 | 1 | [3, 30, 300] | 30 | 1 | | 130 + 1 | 1 | [3, 30, 300] | 30 | null | | 130 + 1 | 1 | [3, 30, 300] | 30 | 2 | | 130 + 1 | 1 | [3, 30, 300] | 300 | 1 | | 400 + 1 | 1 | [3, 30, 300] | 300 | null | | 400 + 1 | 1 | [3, 30, 300] | 300 | 2 | | 400 + 2 | 2 | | | | | + 3 | | | | | | +(20 rows) + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + x | y | y | z +---+---+--------------+--- + 2 | 1 | [1, 2, 3] | 1 + 2 | 1 | [1, 2, 3] | 2 + 2 | 1 | [1, 2, 3] | 3 + 3 | 1 | [1, 2, 3] | 1 + 3 | 1 | [1, 2, 3] | 2 + 3 | 1 | [1, 2, 3] | 3 + 3 | 1 | [2, 3, 4, 5] | 2 + 3 | 1 | [2, 3, 4, 5] | 3 + 3 | 1 | [2, 3, 4, 5] | 4 + 3 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [1, 2, 3] | 1 + 4 | 1 | [1, 2, 3] | 2 + 4 | 1 | [1, 2, 3] | 3 + 4 | 1 | [2, 3, 4, 5] | 2 + 4 | 1 | [2, 3, 4, 5] | 3 + 4 | 1 | [2, 3, 4, 5] | 4 + 4 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [3, 4, 5, 6] | 3 + 4 | 1 | [3, 4, 5, 6] | 4 + 4 | 1 | [3, 4, 5, 6] | 5 + 4 | 1 | [3, 4, 5, 6] | 6 + 2 | 2 | [1, 2, 3] | 2 + 2 | 2 | [1, 2, 3] | 3 + 3 | 2 | [1, 2, 3] | 2 + 3 | 2 | [1, 2, 3] | 3 + 3 | 2 | [2, 3, 4, 5] | 2 + 3 | 2 | [2, 3, 4, 5] | 3 + 3 | 2 | [2, 3, 4, 5] | 4 + 3 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [1, 2, 3] | 2 + 4 | 2 | [1, 2, 3] | 3 + 4 | 2 | [2, 3, 4, 5] | 2 + 4 | 2 | [2, 3, 4, 5] | 3 + 4 | 2 | [2, 3, 4, 5] | 4 + 4 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [3, 4, 5, 6] | 3 + 4 | 2 | [3, 4, 5, 6] | 4 + 4 | 2 | [3, 4, 5, 6] | 5 + 4 | 2 | [3, 4, 5, 6] | 6 + 2 | 3 | [1, 2, 3] | 3 + 3 | 3 | [1, 2, 3] | 3 + 3 | 3 | [2, 3, 4, 5] | 3 + 3 | 3 | [2, 3, 4, 5] | 4 + 3 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [1, 2, 3] | 3 + 4 | 3 | [2, 3, 4, 5] | 3 + 4 | 3 | [2, 3, 4, 5] | 4 + 4 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [3, 4, 5, 6] | 3 + 4 | 3 | [3, 4, 5, 6] | 4 + 4 | 3 | [3, 4, 5, 6] | 5 + 4 | 3 | [3, 4, 5, 6] | 6 +(52 rows) + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; +ERROR: could not find jsonpath variable "x" +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); + json_value +------------ + foo +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_query +------------ + 123 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); + json_query +------------ + [123] +(1 row) + +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +ERROR: syntax error at or near " " of jsonpath input +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); +ERROR: only string constants supported in JSON_TABLE path specification +LINE 1: SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || '... + ^ diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql index 4f30fa46b9..df4a430d88 100644 --- a/src/test/regress/sql/json_sqljson.sql +++ b/src/test/regress/sql/json_sqljson.sql @@ -9,3 +9,7 @@ SELECT JSON_VALUE(NULL FORMAT JSON, '$'); -- JSON_QUERY SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + +-- JSON_TABLE + +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql index b8a6148b7b..fbd86f2084 100644 --- a/src/test/regress/sql/jsonb_sqljson.sql +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -325,3 +325,632 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; + +-- JSON_TABLE + +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); + +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); + +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); + +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + +-- JSON_TABLE: Test backward parsing + +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); + +\sv jsonb_table_view + +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; + +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); + +-- JSON_TABLE: nested paths and plans + +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; + +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; + +-- JSON_TABLE: plan validation + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; + +-- JSON_TABLE: plan execution + +CREATE TEMP TABLE jsonb_table_test (js jsonb); + +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); + +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; + +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 6e4c026492..642a29d8d5 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1303,6 +1303,7 @@ JsonPathKeyword JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback +JsonPathSpec JsonPathString JsonPathVarCallback JsonPathVariable @@ -1311,6 +1312,17 @@ JsonQuotes JsonReturning JsonSemAction JsonSerializeExpr +JsonTable +JsonTableColumn +JsonTableColumnType +JsonTableContext +JsonTableJoinState +JsonTableParent +JsonTablePlan +JsonTablePlanJoinType +JsonTablePlanType +JsonTableScanState +JsonTableSibling JsonTokenType JsonTransformStringValuesAction JsonTypeCategory @@ -2765,6 +2777,7 @@ TableFunc TableFuncRoutine TableFuncScan TableFuncScanState +TableFuncType TableInfo TableLikeClause TableSampleClause -- 2.35.3
v4-0005-SQL-JSON-query-functions.patch (application/octet-stream, 208.4K) download From 68615d445f6949145a2413208f023ead36572bce Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Fri, 16 Jun 2023 17:49:18 +0900 Subject: [PATCH v4 5/7] SQL/JSON query functions This introduces the SQL/JSON functions for querying JSON data using jsonpath expressions. The functions are: JSON_EXISTS() JSON_QUERY() JSON_VALUE() All of these functions only operate on jsonb. The workaround for now is to cast the argument to jsonb. JSON_EXISTS() tests if the jsonpath expression applied to the jsonb value yields any values. JSON_VALUE() must return a single value, and an error occurs if it tries to return multiple values. JSON_QUERY() must return a json object or array, and there are various WRAPPER options for handling scalar or multi-value results. Both these functions have options for handling EMPTY and ERROR conditions. Nikita Glukhov Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby. Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org --- doc/src/sgml/func.sgml | 147 +++ src/backend/executor/execExpr.c | 432 ++++++++ src/backend/executor/execExprInterp.c | 502 ++++++++- src/backend/jit/llvm/llvmjit_expr.c | 250 +++++ src/backend/jit/llvm/llvmjit_types.c | 5 + src/backend/nodes/makefuncs.c | 15 + src/backend/nodes/nodeFuncs.c | 183 ++++ src/backend/optimizer/path/costsize.c | 3 +- src/backend/optimizer/util/clauses.c | 19 + src/backend/parser/gram.y | 348 ++++++- src/backend/parser/parse_collate.c | 7 + src/backend/parser/parse_expr.c | 515 ++++++++- src/backend/parser/parse_target.c | 15 + src/backend/utils/adt/formatting.c | 44 + src/backend/utils/adt/jsonb.c | 62 ++ src/backend/utils/adt/jsonfuncs.c | 170 ++- src/backend/utils/adt/jsonpath.c | 255 +++++ src/backend/utils/adt/jsonpath_exec.c | 391 ++++++- src/backend/utils/adt/ruleutils.c | 136 +++ src/include/executor/execExpr.h | 172 +++ src/include/executor/executor.h | 1 + src/include/nodes/execnodes.h | 3 + src/include/nodes/makefuncs.h | 1 + src/include/nodes/parsenodes.h | 48 + src/include/nodes/primnodes.h | 109 ++ src/include/parser/kwlist.h | 11 + src/include/utils/formatting.h | 2 +- src/include/utils/jsonb.h | 3 + src/include/utils/jsonfuncs.h | 5 + src/include/utils/jsonpath.h | 27 + src/interfaces/ecpg/preproc/ecpg.trailer | 28 + src/test/regress/expected/json_sqljson.out | 18 + src/test/regress/expected/jsonb_sqljson.out | 1042 +++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/json_sqljson.sql | 11 + src/test/regress/sql/jsonb_sqljson.sql | 327 ++++++ src/tools/pgindent/typedefs.list | 14 + 37 files changed, 5230 insertions(+), 93 deletions(-) create mode 100644 src/test/regress/expected/json_sqljson.out create mode 100644 src/test/regress/expected/jsonb_sqljson.out create mode 100644 src/test/regress/sql/json_sqljson.sql create mode 100644 src/test/regress/sql/jsonb_sqljson.sql diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 9cece06c18..cb36e1463b 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16995,6 +16995,153 @@ array w/o UK? | t </tbody> </tgroup> </table> + + <para> + <xref linkend="functions-sqljson-querying"/> details the SQL/JSON + functions that can be used to query JSON data. + </para> + + <note> + <para> + SQL/JSON paths can only be applied to the <type>jsonb</type> type, so it + might be necessary to cast the <replaceable>context_item</replaceable> + argument of these functions to <type>jsonb</type>. + </para> + </note> + + <table id="functions-sqljson-querying"> + <title>SQL/JSON Query Functions</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="func_table_entry"><para role="func_signature"> + Function signature + </para> + <para> + Description + </para> + <para> + Example(s) + </para></entry> + </row> + </thead> + <tbody> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_exists</primary></indexterm> + <function>json_exists</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>TRUE</literal> | <literal>FALSE</literal> |<literal> UNKNOWN</literal> | <literal>ERROR</literal> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns true if the SQL/JSON <replaceable>path_expression</replaceable> + applied to the <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s yields any items. + The <literal>ON ERROR</literal> clause specifies what is returned if + an error occurs. Note that if the <replaceable>path_expression</replaceable> + is <literal>strict</literal>, an error is generated if it yields no items. + The default value is <literal>UNKNOWN</literal> which causes a NULL + result. + </para> + <para> + <literal>json_exists(jsonb '{"key1": [1,2,3]}', 'strict $.key1[*] ? (@ > 2)')</literal> + <returnvalue>t</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'lax $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>f</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'strict $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>ERROR: jsonpath array subscript is out of bounds</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_value</primary></indexterm> + <function>json_value</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> + <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. The extracted value must be + a single <acronym>SQL/JSON</acronym> scalar item. For results that + are objects or arrays, use the <function>json_query</function> + function instead. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + The <literal>ON EMPTY</literal> clause specifies the behavior if the + <replaceable>path_expression</replaceable> yields no value at all. + The <literal>ON ERROR</literal> clause specifies the behavior if an + error occurs as a result of <type>jsonpath</type> evaluation + (including cast to the output type) or execution of + <literal>ON EMPTY</literal> behavior (that was caused by empty result + of <type>jsonpath</type> evaluation). + </para> + <para> + <literal>json_value(jsonb '"123.45"', '$' RETURNING float)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_value(jsonb '"03:04 2015-02-01"', '$.datetime("HH24:MI&nbsp;YYYY-MM-DD")' RETURNING date)</literal> + <returnvalue>2015-02-01</returnvalue> + </para> + <para> + <literal>json_value(jsonb '[1,2]', 'strict $[*]' DEFAULT 9 ON ERROR)</literal> + <returnvalue>9</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_query</primary></indexterm> + <function>json_query</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional> + <optional> { <literal>WITHOUT</literal> | <literal>WITH</literal> { <literal>CONDITIONAL</literal> | <optional><literal>UNCONDITIONAL</literal></optional> } } <optional> <literal>ARRAY</literal> </optional> <literal>WRAPPER</literal> </optional> + <optional> { <literal>KEEP</literal> | <literal>OMIT</literal> } <literal>QUOTES</literal> <optional> <literal>ON SCALAR STRING</literal> </optional> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. + This function must return a JSON string, so if the path expression + returns multiple SQL/JSON items, you must wrap the result using the + <literal>WITH WRAPPER</literal> clause. If the wrapper is + <literal>UNCONDITIONAL</literal>, an array wrapper will always + be applied, even if the returned value is already a single JSON object + or array, but if it is <literal>CONDITIONAL</literal>, it will not be + applied to a single array or object. <literal>UNCONDITIONAL</literal> + is the default. + If the result is a scalar string, by default the value returned will have + surrounding quotes making it a valid JSON value. However, this behavior + is reversed if <literal>OMIT QUOTES</literal> is specified. + The <literal>ON ERROR</literal> and <literal>ON EMPTY</literal> + clauses have similar semantics to those clauses for + <function>json_value</function>. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + </para> + <para> + <literal>json_query(jsonb '[1,[2,3],null]', 'lax $[*][1]' WITH CONDITIONAL WRAPPER)</literal> + <returnvalue>[3]</returnvalue> + </para></entry> + </row> + </tbody> + </tgroup> + </table> + </sect2> <sect2 id="functions-sqljson-path"> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 6ca4098bef..d3d2ce00d1 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -49,6 +49,7 @@ #include "utils/builtins.h" #include "utils/datum.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -88,6 +89,16 @@ static void ExecBuildAggTransCall(ExprState *state, AggState *aggstate, FunctionCallInfo fcinfo, AggStatePerTrans pertrans, int transno, int setno, int setoff, bool ishash, bool nullcheck); +static void ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch); +static Datum GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null); +static JsonCoercionState *ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull); +static JsonItemCoercionsState *ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull); /* @@ -2411,6 +2422,14 @@ ExecInitExprRec(Expr *node, ExprState *state, break; } + case T_JsonExpr: + { + JsonExpr *jexpr = castNode(JsonExpr, node); + + ExecInitJsonExpr(jexpr, state, resv, resnull, &scratch); + break; + } + case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -4178,3 +4197,416 @@ ExecBuildParamSetEqual(TupleDesc desc, return state; } + +/* + * Push steps to evaluate a JsonExpr and its various subsidiary expressions. + */ +static void +ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch) +{ + JsonExprState *jsestate = palloc0(sizeof(JsonExprState)); + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + ListCell *argexprlc; + ListCell *argnamelc; + int skip_step_off = -1; + int passing_args_step_off = -1; + int coercion_step_off = -1; + int coercion_finish_step_off = -1; + int behavior_step_off = -1; + int onempty_expr_step_off = -1; + int onempty_jump_step_off = -1; + int onerror_expr_step_off = -1; + int onerror_jump_step_off = -1; + List *adjust_jumps = NIL; + ListCell *lc; + ExprEvalStep *as; + + jsestate->jsexpr = jexpr; + + /* + * Add steps to compute formatted_expr, pathspec, and PASSING arg + * expressions as things that must be evaluated *before* the actual JSON + * path expression. + */ + ExecInitExprRec((Expr *) jexpr->formatted_expr, state, + &pre_eval->formatted_expr.value, + &pre_eval->formatted_expr.isnull); + ExecInitExprRec((Expr *) jexpr->path_spec, state, + &pre_eval->pathspec.value, + &pre_eval->pathspec.isnull); + + /* + * Before pushing steps for PASSING args, push a step to decide whether to + * skip evaluating the args and the JSON path expression depending on + * whether either of formatted_expr and pathspec is NULL; see + * ExecEvalJsonExprSkip(). + */ + scratch->opcode = EEOP_JSONEXPR_SKIP; + scratch->d.jsonexpr_skip.jsestate = jsestate; + skip_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* PASSING args. */ + jsestate->pre_eval.args = NIL; + passing_args_step_off = state->steps_len; + forboth(argexprlc, jexpr->passing_values, + argnamelc, jexpr->passing_names) + { + Expr *argexpr = (Expr *) lfirst(argexprlc); + String *argname = lfirst_node(String, argnamelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(argname->sval); + var->typid = exprType((Node *) argexpr); + var->typmod = exprTypmod((Node *) argexpr); + + ExecInitExprRec((Expr *) argexpr, state, &var->value, &var->isnull); + + pre_eval->args = lappend(pre_eval->args, var); + } + + /* + * Step for the actual JSON path evaluation; see ExecEvalJson() and + * ExecEvalJsonExpr(). + */ + scratch->opcode = EEOP_JSONEXPR_PATH; + scratch->d.jsonexpr.jsestate = jsestate; + ExprEvalPushStep(state, scratch); + + /* + * Push steps to control the evaluation of expressions based on the result + * of JSON path evaluation. + */ + + /* + * Step to handle ON ERROR and ON EMPTY behavior; see + * ExecEvalJsonExprBehavior(). + */ + scratch->opcode = EEOP_JSONEXPR_BEHAVIOR; + scratch->d.jsonexpr_behavior.jsestate = jsestate; + behavior_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Step(s) to evaluate ON EMPTY expression */ + onempty_expr_step_off = state->steps_len; + if (jexpr->on_empty && + jexpr->on_empty->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_empty->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_empty->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_empty, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onempty_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + } + } + + /* Step(s) to evaluate ON ERROR expression */ + onerror_expr_step_off = state->steps_len; + if (jexpr->on_error && + jexpr->on_error->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_error->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_error->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_error, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onerror_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set later */ + ExprEvalPushStep(state, scratch); + } + } + + /* + * Step to handle applying coercion to the JSON item returned by + * EEOP_JSONEXPR_PATH or to the ON EMPTY/ERROR expression as + * EEOP_JSONEXPR_BEHAVIOR decides; see ExecEvalJsonExprCoercion(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Initialize coercion expression(s). */ + if (jexpr->result_coercion) + { + jsestate->result_jcstate = + ExecInitJsonCoercion(scratch, state, jexpr->result_coercion, + resv, resnull); + /* See the comment above. */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* computed later */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + if (jexpr->coercions) + { + /* + * ExecPrepareJsonItemCoercion() called by ExecEvalJsonExpr() chooses + * one for a given JSON item returned by JsonPathValue(). + */ + jsestate->item_jcstates = + ExecInitJsonItemCoercions(scratch, state, jexpr->coercions, + resv, resnull); + } + + /* + * And a step to clean up after the coercion step; see + * ExecEvalJsonExprCoercionFinish(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION_FINISH; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_finish_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* + * Adjust jump target addresses in various post-eval steps now that we + * have all the steps in place. + */ + + /* EEOP_JSONEXPR_SKIP */ + Assert(skip_step_off >= 0); + as = &state->steps[skip_step_off]; + as->d.jsonexpr_skip.jump_coercion = coercion_step_off; + as->d.jsonexpr_skip.jump_passing_args = passing_args_step_off; + + /* EEOP_JSONEXPR_BEHAVIOR */ + Assert(behavior_step_off >= 0); + as = &state->steps[behavior_step_off]; + as->d.jsonexpr_behavior.jump_onerror_expr = onerror_expr_step_off; + as->d.jsonexpr_behavior.jump_onempty_expr = onempty_expr_step_off; + as->d.jsonexpr_behavior.jump_coercion = coercion_step_off; + as->d.jsonexpr_behavior.jump_skip_coercion = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION */ + Assert(coercion_step_off >= 0); + as = &state->steps[coercion_step_off]; + as->d.jsonexpr_coercion.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION_FINISH */ + Assert(coercion_finish_step_off >= 0); + as = &state->steps[coercion_finish_step_off]; + as->d.jsonexpr_coercion_finish.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion_finish.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JUMP steps */ + + /* + * Ones after ON EMPTY and ON ERROR non-default expressions should jump to + * the coercion step. + */ + if (onempty_jump_step_off >= 0) + { + as = &state->steps[onempty_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + if (onerror_jump_step_off >= 0) + { + as = &state->steps[onerror_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + + /* The rest should jump to the end. */ + foreach(lc, adjust_jumps) + { + as = &state->steps[lfirst_int(lc)]; + as->d.jump.jumpdone = state->steps_len; + } + + /* + * Set RETURNING type's input function used by ExecEvalJsonExprCoercion(). + */ + if (jexpr->omit_quotes || + (jexpr->result_coercion && jexpr->result_coercion->via_io)) + { + Oid typinput; + FmgrInfo *finfo; + + /* lookup the result type's input function */ + getTypeInputInfo(jexpr->returning->typid, &typinput, + &jsestate->input.typioparam); + finfo = palloc0(sizeof(FmgrInfo)); + fmgr_info(typinput, finfo); + jsestate->input.finfo = finfo; + } +} + +/* + * Evaluate a JSON error/empty behavior result. + */ +static Datum +GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null) +{ + *is_null = false; + + switch (behavior->btype) + { + case JSON_BEHAVIOR_EMPTY_ARRAY: + return JsonbPGetDatum(JsonbMakeEmptyArray()); + + case JSON_BEHAVIOR_EMPTY_OBJECT: + return JsonbPGetDatum(JsonbMakeEmptyObject()); + + case JSON_BEHAVIOR_TRUE: + return BoolGetDatum(true); + + case JSON_BEHAVIOR_FALSE: + return BoolGetDatum(false); + + case JSON_BEHAVIOR_NULL: + case JSON_BEHAVIOR_UNKNOWN: + case JSON_BEHAVIOR_EMPTY: + *is_null = true; + return (Datum) 0; + + case JSON_BEHAVIOR_DEFAULT: + /* Always handled in the caller. */ + Assert(false); + return (Datum) 0; + + default: + elog(ERROR, "unrecognized SQL/JSON behavior %d", behavior->btype); + return (Datum) 0; + } +} + +/* Initialize one JsonCoercion for execution. */ +static JsonCoercionState * +ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull) +{ + JsonCoercionState *jcstate = palloc0(sizeof(JsonCoercionState)); + + jcstate->coercion = coercion; + if (coercion && coercion->expr) + { + Datum *save_innermost_caseval; + bool *save_innermost_casenull; + + jcstate->jump_eval_expr = state->steps_len; + + /* Push step(s) to compute cstate->coercion. */ + save_innermost_caseval = state->innermost_caseval; + save_innermost_casenull = state->innermost_casenull; + + state->innermost_caseval = resv; + state->innermost_casenull = resnull; + + ExecInitExprRec((Expr *) coercion->expr, state, resv, resnull); + + state->innermost_caseval = save_innermost_caseval; + state->innermost_casenull = save_innermost_casenull; + } + else + jcstate->jump_eval_expr = -1; + + return jcstate; +} + +/* + * Push steps to evaluate coercions from a given JsonItemCoercions, which + * contains all possible coercions that may need to be applied to JSON + * items coming from evaluating the parent JsonExpr. + */ +static JsonItemCoercionsState * +ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull) +{ + JsonCoercion **coercion; + JsonItemCoercionsState *item_jcstates = palloc0(sizeof(JsonItemCoercionsState)); + JsonCoercionState **item_jcstate; + ExprEvalStep *as; + List *adjust_jumps = NIL; + ListCell *lc; + + /* Push the steps of individual coercions. */ + for (coercion = &coercions->null, + item_jcstate = &item_jcstates->null; + coercion <= &coercions->composite; + coercion++, item_jcstate++) + { + *item_jcstate = ExecInitJsonCoercion(scratch, state, *coercion, + resv, resnull); + + /* Emit JUMP step to skip past other coercions' steps. */ + scratch->opcode = EEOP_JUMP; + + /* + * Remember JUMP step address to set the actual jump target address + * below. + */ + adjust_jumps = lappend_int(adjust_jumps, state->steps_len); + ExprEvalPushStep(state, scratch); + } + + foreach(lc, adjust_jumps) + { + int jump_step_id = lfirst_int(lc); + + as = &state->steps[jump_step_id]; + as->d.jump.jumpdone = state->steps_len; + } + + return item_jcstates; +} diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 76e59691e5..4c97e714ea 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -74,6 +74,7 @@ #include "utils/json.h" #include "utils/jsonb.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/timestamp.h" @@ -152,6 +153,9 @@ static TupleDesc get_cached_rowtype(Oid type_id, int32 typmod, bool *changed); static void ExecEvalRowNullInt(ExprState *state, ExprEvalStep *op, ExprContext *econtext, bool checkisnull); +static Datum ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate); /* fast-path evaluation functions */ static Datum ExecJustInnerVar(ExprState *state, ExprContext *econtext, bool *isnull); @@ -480,6 +484,11 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_XMLEXPR, &&CASE_EEOP_JSON_CONSTRUCTOR, &&CASE_EEOP_IS_JSON, + &&CASE_EEOP_JSONEXPR_SKIP, + &&CASE_EEOP_JSONEXPR_PATH, + &&CASE_EEOP_JSONEXPR_BEHAVIOR, + &&CASE_EEOP_JSONEXPR_COERCION, + &&CASE_EEOP_JSONEXPR_COERCION_FINISH, &&CASE_EEOP_AGGREF, &&CASE_EEOP_GROUPING_FUNC, &&CASE_EEOP_WINDOW_FUNC, @@ -1186,8 +1195,14 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) /* second and third arguments are already set up */ fcinfo_in->isnull = false; + + /* Pass down soft-error capture node if any. */ + fcinfo_in->context = state->escontext; + d = FunctionCallInvoke(fcinfo_in); *op->resvalue = d; + if (SOFT_ERROR_OCCURRED(state->escontext)) + *op->resnull = true; /* Should get null result if and only if str is NULL */ if (str == NULL) @@ -1195,7 +1210,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) Assert(*op->resnull); Assert(fcinfo_in->isnull); } - else + else if (!SOFT_ERROR_OCCURRED(state->escontext)) { Assert(!*op->resnull); Assert(!fcinfo_in->isnull); @@ -1543,6 +1558,38 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_JSONEXPR_PATH) + { + /* too complex for an inline implementation */ + ExecEvalJsonExpr(state, op, econtext); + EEO_NEXT(); + } + + EEO_CASE(EEOP_JSONEXPR_SKIP) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprSkip(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_BEHAVIOR) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprBehavior(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercion(state, op, econtext, + *op->resvalue, *op->resnull)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION_FINISH) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercionFinish(state, op)); + } + EEO_CASE(EEOP_AGGREF) { /* @@ -3745,7 +3792,7 @@ ExecEvalConstraintCheck(ExprState *state, ExprEvalStep *op) { if (!*op->d.domaincheck.checknull && !DatumGetBool(*op->d.domaincheck.checkvalue)) - ereport(ERROR, + errsave(state->escontext, (errcode(ERRCODE_CHECK_VIOLATION), errmsg("value for domain %s violates check constraint \"%s\"", format_type_be(op->d.domaincheck.resulttype), @@ -4138,6 +4185,457 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op) *op->resvalue = BoolGetDatum(res); } +/* + * Evaluate given JsonExpr by performing the specified JSON operation. + * + * This also populates the JsonExprPostEvalState with the information needed + * by the subsequent steps that handle the specified JsonBehavior. + */ +void +ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) +{ + JsonExprState *jsestate = op->d.jsonexpr.jsestate; + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonExpr *jexpr = jsestate->jsexpr; + Datum item; + Datum res = (Datum) 0; + bool resnull = true; + JsonPath *path; + bool *error; + bool *empty; + + item = pre_eval->formatted_expr.value; + path = DatumGetJsonPathP(pre_eval->pathspec.value); + + /* Reset JsonExprPostEvalState for this evaluation. */ + memset(post_eval, 0, sizeof(*post_eval)); + + /* Respect ON ERROR behavior during path evaluation. */ + jsestate->throw_error = (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR); + + /* + * Be sure to save the error (if needed) and empty statuses for the + * EEOP_JSONEXPR_BEHAVIOR step to peruse. + */ + error = !jsestate->throw_error ? &post_eval->error : NULL; + empty = &post_eval->empty; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + res = JsonPathQuery(item, path, jexpr->wrapper, empty, error, + pre_eval->args); + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + resnull = !DatumGetPointer(res); + break; + + case JSON_VALUE_OP: + { + JsonbValue *jbv = JsonPathValue(item, path, empty, error, + pre_eval->args); + + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + if (!jbv) /* NULL or empty */ + { + resnull = true; + break; + } + + Assert(!*empty); + + resnull = false; + + /* coerce scalar item to the output type */ + if (jexpr->returning->typid == JSONOID || + jexpr->returning->typid == JSONBOID) + { + /* Use result coercion from json[b] to the output type */ + res = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + break; + } + + /* + * Error out if no cast exists to coerce SQL/JSON item to the + * the output type. + */ + Assert(post_eval->item_jcstate == NULL); + res = ExecPrepareJsonItemCoercion(jbv, + jsestate->item_jcstates, + &post_eval->item_jcstate); + if (post_eval->item_jcstate && + post_eval->item_jcstate->coercion && + (post_eval->item_jcstate->coercion->via_io || + post_eval->item_jcstate->coercion->via_populate)) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * Coercion via I/O means here that the cast to the target + * type simply does not exist. + */ + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE), + errmsg("SQL/JSON item cannot be cast to target type"))); + } + break; + } + + case JSON_EXISTS_OP: + { + bool exists = JsonPathExists(item, path, + pre_eval->args, + error); + + resnull = error && *error; + res = BoolGetDatum(exists); + break; + } + + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * If the ON EMPTY behavior is to cause an error, do so here. Other + * behaviors will be handled by the caller. + */ + if (*empty) + { + Assert(jexpr->on_empty); /* it is not JSON_EXISTS */ + + if (jexpr->on_empty->btype == JSON_BEHAVIOR_ERROR) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + ereport(ERROR, + (errcode(ERRCODE_NO_SQL_JSON_ITEM), + errmsg("no SQL/JSON item"))); + } + } + + *op->resvalue = res; + *op->resnull = resnull; +} + +/* + * Skip calling ExecEvalJson() on the given JsonExpr? + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_skip.jsestate; + + /* + * Skip if either of the input expressions has turned out to be NULL, + * though do execute domain checks for NULLs, which are handled by the + * coercion step. + */ + if (jsestate->pre_eval.formatted_expr.isnull || + jsestate->pre_eval.pathspec.isnull) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_skip.jump_coercion; + } + + /* + * Go evaluate the PASSING args if any and subsequently JSON path itself. + */ + return op->d.jsonexpr_skip.jump_passing_args; +} + +/* + * Returns the step address to perform the JsonBehavior applicable to + * the JSON item that resulted from evaluating the given JsonExpr. + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_behavior.jsestate; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonBehavior *behavior = NULL; + int jump_to = -1; + + if (post_eval->error || post_eval->coercion_error) + { + behavior = jsestate->jsexpr->on_error; + jump_to = op->d.jsonexpr_behavior.jump_onerror_expr; + } + else if (post_eval->empty) + { + behavior = jsestate->jsexpr->on_empty; + jump_to = op->d.jsonexpr_behavior.jump_onempty_expr; + } + else if (!post_eval->coercion_done) + { + /* + * If no error or the JSON item is not empty, directly go to the + * coercion step to coerce the item as is. + */ + return op->d.jsonexpr_behavior.jump_coercion; + } + + Assert(behavior); + + /* + * Set up for coercion step that will run to coerce a non-default behavior + * value. It should use result_coercion, if any. Errors that may occur + * should be thrown for JSON ops other than JSON_VALUE_OP. + */ + if (behavior->btype != JSON_BEHAVIOR_DEFAULT) + { + post_eval->item_jcstate = NULL; + jsestate->throw_error = (jsestate->jsexpr->op != JSON_VALUE_OP); + } + + Assert(jump_to >= 0); + return jump_to; +} + +/* + * Evaluate or return the step address to evaluate a coercion of a JSON item + * to the target type. The former if the coercion must be done right away by + * calling its input function or by calling json_populate_type(). + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull) +{ + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonExpr *jexpr = jsestate->jsexpr; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonCoercionState *item_jcstate = post_eval->item_jcstate; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + + if (item_jcstate == NULL && + jsestate->jsexpr->op != JSON_EXISTS_OP) + { + ErrorSaveContext escontext = {T_ErrorSaveContext}; + Node *escontext_p; + JsonCoercion *coercion; + Jsonb *jb; + + escontext_p = !jsestate->throw_error ? (Node *) &escontext : NULL; + coercion = result_jcstate ? result_jcstate->coercion : NULL; + jb = resnull ? NULL : DatumGetJsonbP(res); + + if ((coercion && coercion->via_io) || + (jexpr->omit_quotes && !resnull && + JB_ROOT_IS_SCALAR(jb))) + { + /* strip quotes and call typinput function */ + char *str = resnull ? NULL : JsonbUnquote(jb); + bool type_is_domain; + + type_is_domain = (getBaseType(jexpr->returning->typid) != + jexpr->returning->typid); + + /* + * Catch errors only if the type is not a domain, because errors + * caused by a domain's constraint failure must be thrown right + * away. + */ + if (!InputFunctionCallSafe(jsestate->input.finfo, str, + jsestate->input.typioparam, + jexpr->returning->typmod, + !type_is_domain ? escontext_p : NULL, + op->resvalue)) + { + post_eval->error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + else if (coercion && coercion->via_populate) + { + *op->resvalue = json_populate_type(res, JSONBOID, + jexpr->returning->typid, + jexpr->returning->typmod, + &post_eval->cache, + econtext->ecxt_per_query_memory, + op->resnull, + escontext_p); + if (SOFT_ERROR_OCCURRED(escontext_p)) + { + post_eval->error = true; + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + } + + if (!jsestate->throw_error) + { + post_eval->escontext.type = T_ErrorSaveContext; + state->escontext = (Node *) &post_eval->escontext; + } + + if (item_jcstate && item_jcstate->jump_eval_expr >= 0) + return item_jcstate->jump_eval_expr; + else if (result_jcstate && result_jcstate->jump_eval_expr >= 0) + return result_jcstate->jump_eval_expr; + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; +} + +/* + * Checks if the coercion evaluation led to an error. If an error did occur, + * this returns the address of the step that handles the error, otherwise + * the step after the coercion step, which finishes the JsonExpr processing. + */ +int +ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op) +{ + JsonExprPostEvalState *post_eval = + &op->d.jsonexpr_coercion_finish.jsestate->post_eval; + + if (SOFT_ERROR_OCCURRED(state->escontext)) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + post_eval->coercion_error = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_error; + } + + /* Reset. */ + state->escontext = NULL; + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_done; +} + +/* + * Prepare SQL/JSON item coercion to the output type. Returned a datum of the + * corresponding SQL type and a pointer to the coercion state. + */ +static Datum +ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate) +{ + JsonCoercionState *item_jcstate; + Datum res; + JsonbValue buf; + + if (item->type == jbvBinary && + JsonContainerIsScalar(item->val.binary.data)) + { + bool res PG_USED_FOR_ASSERTS_ONLY; + + res = JsonbExtractScalar(item->val.binary.data, &buf); + item = &buf; + Assert(res); + } + + /* get coercion state reference and datum of the corresponding SQL type */ + switch (item->type) + { + case jbvNull: + item_jcstate = item_jcstates->null; + res = (Datum) 0; + break; + + case jbvString: + item_jcstate = item_jcstates->string; + res = PointerGetDatum(cstring_to_text_with_len(item->val.string.val, + item->val.string.len)); + break; + + case jbvNumeric: + item_jcstate = item_jcstates->numeric; + res = NumericGetDatum(item->val.numeric); + break; + + case jbvBool: + item_jcstate = item_jcstates->boolean; + res = BoolGetDatum(item->val.boolean); + break; + + case jbvDatetime: + res = item->val.datetime.value; + switch (item->val.datetime.typid) + { + case DATEOID: + item_jcstate = item_jcstates->date; + break; + case TIMEOID: + item_jcstate = item_jcstates->time; + break; + case TIMETZOID: + item_jcstate = item_jcstates->timetz; + break; + case TIMESTAMPOID: + item_jcstate = item_jcstates->timestamp; + break; + case TIMESTAMPTZOID: + item_jcstate = item_jcstates->timestamptz; + break; + default: + elog(ERROR, "unexpected jsonb datetime type oid %u", + item->val.datetime.typid); + return (Datum) 0; + } + break; + + case jbvArray: + case jbvObject: + case jbvBinary: + item_jcstate = item_jcstates->composite; + res = JsonbPGetDatum(JsonbValueToJsonb(item)); + break; + + default: + elog(ERROR, "unexpected jsonb value type %d", item->type); + return (Datum) 0; + } + + *p_item_jcstate = item_jcstate; + + return res; +} + /* * ExecEvalGroupingFunc diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 00d7b8110b..da3870cba6 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -1860,6 +1860,256 @@ llvm_compile_expr(ExprState *state) LLVMBuildBr(b, opblocks[opno + 1]); break; + case EEOP_JSONEXPR_PATH: + build_EvalXFunc(b, mod, "ExecEvalJsonExpr", + v_state, op, v_econtext); + LLVMBuildBr(b, opblocks[opno + 1]); + break; + + case EEOP_JSONEXPR_SKIP: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprSkip() to decide if JSON path + * evaluation can be skipped. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprSkip"), + params, lengthof(params), ""); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_skip.jump_coercion, which signifies + * skipping of JSON path evaluation, else to the next step + * which must point to the steps to evaluate PASSING args, + * if any, or to the JSON path evaluation. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_skip.jump_coercion), + ""), + opblocks[op->d.jsonexpr_skip.jump_coercion], + opblocks[opno + 1]); + break; + } + + case EEOP_JSONEXPR_BEHAVIOR: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + LLVMBasicBlockRef b_jump_onerror_default; + + /* + * Call ExecEvalJsonExprBehavior() to decide if ON EMPTY + * or ON ERROR behavior must be invoked depending on what + * JSON path evaluation returned. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprBehavior"), + params, lengthof(params), ""); + + b_jump_onerror_default = + l_bb_before_v(opblocks[opno + 1], + "op.%d.jsonexpr_behavior_jump_onerror_default", opno); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_behavior.jump_coercion, else to the + * next block, one that checks whether to evaluate the ON + * ERROR default expression. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_coercion), + ""), + opblocks[op->d.jsonexpr_behavior.jump_coercion], + b_jump_onerror_default); + + /* + * Block that checks whether to evaluate the ON ERROR + * default expression. + * + * Jump to evaluate the ON ERROR default expression if the + * returned address is the same as + * jsonexpr_behavior.jump_onerror_default, else jump to + * evaluate the ON EMPTY default expression. + */ + LLVMPositionBuilderAtEnd(b, b_jump_onerror_default); + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_onerror_expr), + ""), + opblocks[op->d.jsonexpr_behavior.jump_onerror_expr], + opblocks[op->d.jsonexpr_behavior.jump_onempty_expr]); + break; + } + case EEOP_JSONEXPR_COERCION: + { + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonItemCoercionsState *item_jcstates = jsestate->item_jcstates; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + LLVMValueRef v_ret; + LLVMValueRef params[5]; + + /* + * Call ExecEvalJsonExprCoercion() to evaluate appropriate + * coercion. This will return the step address to jump + * to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + params[2] = v_econtext; + params[3] = v_resvaluep; + params[4] = v_resnullp; + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercion"), + params, lengthof(params), ""); + + /* + * Jump to handle a coercion error if the returned address + * is the same as jsonexpr_coercion.jump_coercion_error, + * else to the step after coercion (coercion done!). + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + + if (item_jcstates) + { + JsonCoercionState **item_jcstate; + int n_coercions = (int) + (item_jcstates->composite - item_jcstates->null) + 1; + int i; + LLVMBasicBlockRef *b_coercions; + + + /* + * Will create a block for each coercion below to + * check whether to evaluate the coercion's expression + * if there's one or to skip to the end if not. + */ + b_coercions = palloc((n_coercions + 1) * sizeof(LLVMBasicBlockRef)); + for (i = 0; i < n_coercions + 1; i++) + b_coercions[i] = + l_bb_before_v(opblocks[opno + 1], + "op.%d.json_item_coercion.%d", + opno, i); + + /* Jump to check first coercion */ + LLVMBuildBr(b, b_coercions[0]); + + /* + * Add conditional branches for individual coercion's + * expressions + */ + for (item_jcstate = &item_jcstates->null, i = 0; + item_jcstate <= &item_jcstates->composite; + item_jcstate++, i++) + { + /* Block for this coercion */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + + /* + * Jump to evaluate the coercion's expression if + * the address returned is the same as this + * coercion's jump_eval_expr (that is, if it is + * valid), else check the next coercion's. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const((*item_jcstate)->jump_eval_expr), + ""), + (*item_jcstate)->jump_eval_expr >= 0 ? + opblocks[(*item_jcstate)->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + b_coercions[i + 1]); + } + + /* + * A placeholder block that the last coercion's block + * might jump to, which unconditionally jumps to end + * of coercions. + */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + LLVMBuildBr(b, opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + + /* + * Jump to evaluate the result_coercion's expression if + * none of the above coercions matched, that is, if + * there's one. + */ + if (result_jcstate) + { + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(result_jcstate->jump_eval_expr), + ""), + result_jcstate->jump_eval_expr >= 0 ? + opblocks[result_jcstate->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + break; + } + + case EEOP_JSONEXPR_COERCION_FINISH: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprCoercionFinish() to check whether + * an coercion error occurred, in which case we must jump + * to whatever step handles the error. This returns the + * step address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercionFinish"), + params, lengthof(params), ""); + + /* + * Jump to the step that handles coercion error if the + * returned address is the same as + * jsonexpr_coercion_finish.jump_coercion_error, else to + * jsonexpr_coercion_finish.jump_coercion_done. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion_finish.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_done]); + break; + } + case EEOP_AGGREF: { LLVMValueRef v_aggno; diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c index 41ac4c6f45..cf3ced3427 100644 --- a/src/backend/jit/llvm/llvmjit_types.c +++ b/src/backend/jit/llvm/llvmjit_types.c @@ -135,6 +135,11 @@ void *referenced_functions[] = ExecEvalXmlExpr, ExecEvalJsonConstructor, ExecEvalJsonIsPredicate, + ExecEvalJsonExprSkip, + ExecEvalJsonExprBehavior, + ExecEvalJsonExprCoercion, + ExecEvalJsonExprCoercionFinish, + ExecEvalJsonExpr, MakeExpandedObjectReadOnlyInternal, slot_getmissingattrs, slot_getsomeattrs_int, diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index c6c310d253..7bcc12b04f 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -863,6 +863,21 @@ makeJsonValueExpr(Expr *expr, JsonFormat *format) return jve; } +/* + * makeJsonBehavior - + * creates a JsonBehavior node + */ +JsonBehavior * +makeJsonBehavior(JsonBehaviorType type, Node *default_expr) +{ + JsonBehavior *behavior = makeNode(JsonBehavior); + + behavior->btype = type; + behavior->default_expr = default_expr; + + return behavior; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index dda964bd19..d31371bb4d 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -236,6 +236,12 @@ exprType(const Node *expr) case T_JsonIsPredicate: type = BOOLOID; break; + case T_JsonExpr: + type = ((const JsonExpr *) expr)->returning->typid; + break; + case T_JsonCoercion: + type = exprType(((const JsonCoercion *) expr)->expr); + break; case T_NullTest: type = BOOLOID; break; @@ -495,6 +501,10 @@ exprTypmod(const Node *expr) return exprTypmod((Node *) ((const JsonValueExpr *) expr)->formatted_expr); case T_JsonConstructorExpr: return ((const JsonConstructorExpr *) expr)->returning->typmod; + case T_JsonExpr: + return ((JsonExpr *) expr)->returning->typmod; + case T_JsonCoercion: + return exprTypmod(((const JsonCoercion *) expr)->expr); case T_CoerceToDomain: return ((const CoerceToDomain *) expr)->resulttypmod; case T_CoerceToDomainValue: @@ -971,6 +981,22 @@ exprCollation(const Node *expr) /* IS JSON's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + coll = InvalidOid; + else if (coercion->expr) + coll = exprCollation(coercion->expr); + else if (coercion->via_io || coercion->via_populate) + coll = coercion->collation; + else + coll = InvalidOid; + } + break; + case T_NullTest: /* NullTest's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ @@ -1207,6 +1233,21 @@ exprSetCollation(Node *expr, Oid collation) case T_JsonIsPredicate: Assert(!OidIsValid(collation)); /* result is always boolean */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + Assert(!OidIsValid(collation)); + else if (coercion->expr) + exprSetCollation(coercion->expr, collation); + else if (coercion->via_io || coercion->via_populate) + coercion->collation = collation; + else + Assert(!OidIsValid(collation)); + } + break; case T_NullTest: /* NullTest's result is boolean ... */ Assert(!OidIsValid(collation)); /* ... so never set a collation */ @@ -1510,6 +1551,15 @@ exprLocation(const Node *expr) case T_JsonIsPredicate: loc = ((const JsonIsPredicate *) expr)->location; break; + case T_JsonExpr: + { + const JsonExpr *jsexpr = (const JsonExpr *) expr; + + /* consider both function name and leftmost arg */ + loc = leftmostLoc(jsexpr->location, + exprLocation(jsexpr->formatted_expr)); + } + break; case T_NullTest: { const NullTest *nexpr = (const NullTest *) expr; @@ -2262,6 +2312,54 @@ expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + if (WALK(jexpr->formatted_expr)) + return true; + if (WALK(jexpr->result_coercion)) + return true; + if (WALK(jexpr->passing_values)) + return true; + /* we assume walker doesn't care about passing_names */ + if (jexpr->on_empty && + WALK(jexpr->on_empty->default_expr)) + return true; + if (WALK(jexpr->on_error->default_expr)) + return true; + if (WALK(jexpr->coercions)) + return true; + } + break; + case T_JsonCoercion: + return WALK(((JsonCoercion *) node)->expr); + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + + if (WALK(coercions->null)) + return true; + if (WALK(coercions->string)) + return true; + if (WALK(coercions->numeric)) + return true; + if (WALK(coercions->boolean)) + return true; + if (WALK(coercions->date)) + return true; + if (WALK(coercions->time)) + return true; + if (WALK(coercions->timetz)) + return true; + if (WALK(coercions->timestamp)) + return true; + if (WALK(coercions->timestamptz)) + return true; + if (WALK(coercions->composite)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: @@ -3261,6 +3359,54 @@ expression_tree_mutator_impl(Node *node, return (Node *) newnode; } + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + JsonExpr *newnode; + + FLATCOPY(newnode, jexpr, JsonExpr); + MUTATE(newnode->path_spec, jexpr->path_spec, Node *); + MUTATE(newnode->formatted_expr, jexpr->formatted_expr, Node *); + MUTATE(newnode->result_coercion, jexpr->result_coercion, JsonCoercion *); + MUTATE(newnode->passing_values, jexpr->passing_values, List *); + /* assume mutator does not care about passing_names */ + if (newnode->on_empty) + MUTATE(newnode->on_empty->default_expr, + jexpr->on_empty->default_expr, Node *); + MUTATE(newnode->on_error->default_expr, + jexpr->on_error->default_expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonCoercion: + { + JsonCoercion *coercion = (JsonCoercion *) node; + JsonCoercion *newnode; + + FLATCOPY(newnode, coercion, JsonCoercion); + MUTATE(newnode->expr, coercion->expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + JsonItemCoercions *newnode; + + FLATCOPY(newnode, coercions, JsonItemCoercions); + MUTATE(newnode->null, coercions->null, JsonCoercion *); + MUTATE(newnode->string, coercions->string, JsonCoercion *); + MUTATE(newnode->numeric, coercions->numeric, JsonCoercion *); + MUTATE(newnode->boolean, coercions->boolean, JsonCoercion *); + MUTATE(newnode->date, coercions->date, JsonCoercion *); + MUTATE(newnode->time, coercions->time, JsonCoercion *); + MUTATE(newnode->timetz, coercions->timetz, JsonCoercion *); + MUTATE(newnode->timestamp, coercions->timestamp, JsonCoercion *); + MUTATE(newnode->timestamptz, coercions->timestamptz, JsonCoercion *); + MUTATE(newnode->composite, coercions->composite, JsonCoercion *); + return (Node *) newnode; + } + break; case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -3947,6 +4093,43 @@ raw_expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonArgument: + return WALK(((JsonArgument *) node)->val); + case T_JsonCommon: + { + JsonCommon *jc = (JsonCommon *) node; + + if (WALK(jc->expr)) + return true; + if (WALK(jc->pathspec)) + return true; + if (WALK(jc->passing)) + return true; + } + break; + case T_JsonBehavior: + { + JsonBehavior *jb = (JsonBehavior *) node; + + if (jb->btype == JSON_BEHAVIOR_DEFAULT && + WALK(jb->default_expr)) + return true; + } + break; + case T_JsonFuncExpr: + { + JsonFuncExpr *jfe = (JsonFuncExpr *) node; + + if (WALK(jfe->common)) + return true; + if (jfe->output && WALK(jfe->output)) + return true; + if (WALK(jfe->on_empty)) + return true; + if (WALK(jfe->on_error)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index ef475d95a1..f58c275b4b 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -4609,7 +4609,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context) IsA(node, SQLValueFunction) || IsA(node, XmlExpr) || IsA(node, CoerceToDomain) || - IsA(node, NextValueExpr)) + IsA(node, NextValueExpr) || + IsA(node, JsonExpr)) { /* Treat all these as having cost 1 */ context->total.per_tuple += cpu_operator_cost; diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index da258968b8..e40cfab4b7 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -53,6 +53,7 @@ #include "utils/fmgroids.h" #include "utils/json.h" #include "utils/jsonb.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/syscache.h" @@ -412,6 +413,24 @@ contain_mutable_functions_walker(Node *node, void *context) /* Check all subnodes */ } + if (IsA(node, JsonExpr)) + { + JsonExpr *jexpr = castNode(JsonExpr, node); + Const *cnst; + + if (!IsA(jexpr->path_spec, Const)) + return true; + + cnst = castNode(Const, jexpr->path_spec); + + Assert(cnst->consttype == JSONPATHOID); + if (cnst->constisnull) + return false; + + return jspIsMutable(DatumGetJsonPathP(cnst->constvalue), + jexpr->passing_names, jexpr->passing_values); + } + if (IsA(node, SQLValueFunction)) { /* all variants of SQLValueFunction are stable */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 341b002dc7..f7ad8f18de 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -278,6 +278,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MergeWhenClause *mergewhen; struct KeyActions *keyactions; struct KeyAction *keyaction; + JsonBehavior *jsbehavior; + JsonQuotes js_quotes; } %type <node> stmt toplevel_stmt schema_stmt routine_body_stmt @@ -650,14 +652,22 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_returning_clause_opt json_name_and_value json_aggregate_func + json_api_common_syntax + json_argument %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt + json_arguments %type <ival> json_encoding_clause_opt json_predicate_type_constraint + json_wrapper_behavior %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt +%type <jsbehavior> json_value_behavior + json_query_behavior + json_exists_behavior +%type <js_quotes> json_quotes_clause_opt /* * Non-keyword token types. These are hard-wired into the "flex" lexer. @@ -694,7 +704,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT - COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT + COMMITTED COMPRESSION CONCURRENTLY CONDITIONAL CONFIGURATION CONFLICT CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY COST CREATE CROSS CSV CUBE CURRENT_P CURRENT_CATALOG CURRENT_DATE CURRENT_ROLE CURRENT_SCHEMA @@ -705,8 +715,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P DOUBLE_P DROP - EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT - EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION + EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE + EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION EXTENSION EXTERNAL EXTRACT FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR @@ -721,10 +731,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION - JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG - JSON_SCALAR JSON_SERIALIZE + JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE - KEY KEYS + KEY KEYS KEEP LABEL LANGUAGE LARGE_P LAST_P LATERAL_P LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL @@ -738,7 +748,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC - OBJECT_P OF OFF OFFSET OIDS OLD ON ONLY OPERATOR OPTION OPTIONS OR + OBJECT_P OF OFF OFFSET OIDS OLD OMIT ON ONLY OPERATOR OPTION OPTIONS OR ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER @@ -747,7 +757,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION - QUOTE + QUOTE QUOTES RANGE READ REAL REASSIGN RECHECK RECURSIVE REF_P REFERENCES REFERENCING REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA @@ -758,7 +768,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); SEQUENCE SEQUENCES SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P - START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P + START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRING_P STRIP_P SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN @@ -766,7 +776,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); TREAT TRIGGER TRIM TRUE_P TRUNCATE TRUSTED TYPE_P TYPES_P - UESCAPE UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN + UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING @@ -15663,6 +15673,192 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_error = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->on_error = $10; + n->location = @1; + $$ = (Node *) n; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->location = @1; + $$ = (Node *) p; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + json_exists_behavior ON ERROR_P + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->on_error = $5; + p->location = @1; + $$ = (Node *) p; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->on_error = $8; + n->location = @1; + $$ = (Node *) n; + } ; @@ -16389,6 +16585,72 @@ opt_asymmetric: ASYMMETRIC ; /* SQL/JSON support */ +json_api_common_syntax: + json_value_expr ',' a_expr /* i.e. a json_path */ + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + + | json_value_expr ',' a_expr /* i.e. a json_path */ + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = $5; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = $7; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_arguments: + json_argument { $$ = list_make1($1); } + | json_arguments ',' json_argument { $$ = lappend($1, $3); } + ; + +json_argument: + json_value_expr AS ColLabel + { + JsonArgument *n = makeNode(JsonArgument); + + n->val = (JsonValueExpr *) $1; + n->name = $3; + $$ = (Node *) n; + } + ; + json_value_expr: a_expr json_format_clause_opt { @@ -16412,6 +16674,50 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; +json_query_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + | EMPTY_P ARRAY { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + | EMPTY_P OBJECT_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + /* non-standard, for Oracle compatibility only */ + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + ; + +json_exists_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | TRUE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_TRUE, NULL); } + | FALSE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_FALSE, NULL); } + | UNKNOWN { $$ = makeJsonBehavior(JSON_BEHAVIOR_UNKNOWN, NULL); } + ; + +json_value_behavior: + NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + ; + +/* ARRAY is a noise word */ +json_wrapper_behavior: + WITHOUT WRAPPER { $$ = JSW_NONE; } + | WITHOUT ARRAY WRAPPER { $$ = JSW_NONE; } + | WITH WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL ARRAY WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL WRAPPER { $$ = JSW_UNCONDITIONAL; } + | /* empty */ { $$ = JSW_NONE; } + ; + +json_quotes_clause_opt: + KEEP QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_KEEP; } + | KEEP QUOTES { $$ = JS_QUOTES_KEEP; } + | OMIT QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_OMIT; } + | OMIT QUOTES { $$ = JS_QUOTES_OMIT; } + | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } + ; + json_returning_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17014,6 +17320,7 @@ unreserved_keyword: | COMMIT | COMMITTED | COMPRESSION + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17050,10 +17357,12 @@ unreserved_keyword: | DOUBLE_P | DROP | EACH + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17103,6 +17412,7 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION + | KEEP | KEY | KEYS | LABEL @@ -17149,6 +17459,7 @@ unreserved_keyword: | OFF | OIDS | OLD + | OMIT | OPERATOR | OPTION | OPTIONS @@ -17179,6 +17490,7 @@ unreserved_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REASSIGN @@ -17238,6 +17550,7 @@ unreserved_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUPPORT @@ -17260,6 +17573,7 @@ unreserved_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNKNOWN | UNLISTEN @@ -17320,10 +17634,13 @@ col_name_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE | LEAST | NATIONAL | NCHAR @@ -17556,6 +17873,7 @@ bare_label_keyword: | COMMITTED | COMPRESSION | CONCURRENTLY + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17608,11 +17926,13 @@ bare_label_keyword: | DROP | EACH | ELSE + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | END_P | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17682,10 +18002,14 @@ bare_label_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE + | KEEP | KEY | KEYS | LABEL @@ -17746,6 +18070,7 @@ bare_label_keyword: | OFF | OIDS | OLD + | OMIT | ONLY | OPERATOR | OPTION @@ -17783,6 +18108,7 @@ bare_label_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REAL @@ -17851,6 +18177,7 @@ bare_label_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUBSTRING @@ -17885,6 +18212,7 @@ bare_label_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNIQUE | UNKNOWN diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c index 9f6afc351c..692e5d1225 100644 --- a/src/backend/parser/parse_collate.c +++ b/src/backend/parser/parse_collate.c @@ -691,6 +691,13 @@ assign_collations_walker(Node *node, assign_collations_context *context) &loccontext); } break; + case T_JsonExpr: + + /* + * Context item and PASSING arguments are already + * marked with collations in parse_expr.c. + */ + break; default: /* diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 8c41c1f1c5..92eda6a960 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -90,6 +90,7 @@ static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); static Node *transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr); +static Node *transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *p); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -353,6 +354,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); break; + case T_JsonFuncExpr: + result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3240,7 +3245,7 @@ makeCaseTestExpr(Node *expr) static Node * transformJsonValueExpr(ParseState *pstate, char *constructName, JsonValueExpr *ve, JsonFormatType default_format, - Oid targettype) + Oid targettype, bool isarg) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3277,6 +3282,35 @@ transformJsonValueExpr(ParseState *pstate, char *constructName, else format = ve->format->format_type; } + else if (isarg) + { + /* Pass SQL/JSON item types directly without conversion to json[b]. */ + switch (exprtype) + { + case TEXTOID: + case NUMERICOID: + case BOOLOID: + case INT2OID: + case INT4OID: + case INT8OID: + case FLOAT4OID: + case FLOAT8OID: + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + return expr; + + default: + if (typcategory == TYPCATEGORY_STRING) + return expr; + /* else convert argument to json[b] type */ + break; + } + + format = default_format; + } else if (exprtype == JSONOID || exprtype == JSONBOID) format = JS_FORMAT_DEFAULT; /* do not format json[b] types */ else @@ -3288,7 +3322,8 @@ transformJsonValueExpr(ParseState *pstate, char *constructName, Node *coerced; bool cast_is_needed = OidIsValid(targettype); - if (!cast_is_needed && + if (!isarg && + !cast_is_needed && exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), @@ -3615,7 +3650,7 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) Node *val = transformJsonValueExpr(pstate, "JSON_OBJECT()", kv->value, JS_FORMAT_DEFAULT, - InvalidOid); + InvalidOid, false); args = lappend(args, key); args = lappend(args, val); @@ -3800,7 +3835,7 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) key = transformExprRecurse(pstate, (Node *) agg->arg->key); val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", agg->arg->value, - JS_FORMAT_DEFAULT, InvalidOid); + JS_FORMAT_DEFAULT, InvalidOid, false); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3856,9 +3891,8 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) Oid aggfnoid; Oid aggtype; - arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", - agg->arg, - JS_FORMAT_DEFAULT, InvalidOid); + arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", agg->arg, + JS_FORMAT_DEFAULT, InvalidOid, false); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3905,9 +3939,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) { JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, "JSON_ARRAY()", - jsval, - JS_FORMAT_DEFAULT, - InvalidOid); + jsval, JS_FORMAT_DEFAULT, + InvalidOid, false); args = lappend(args, val); } @@ -4078,7 +4111,7 @@ transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) * function-like CASTs. */ arg = transformJsonValueExpr(pstate, "JSON()", jsexpr->expr, - JS_FORMAT_JSON, returning->typid); + JS_FORMAT_JSON, returning->typid, false); } return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, @@ -4125,9 +4158,8 @@ static Node * transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) { Node *arg = transformJsonValueExpr(pstate, "JSON_SERIALIZE()", - expr->expr, - JS_FORMAT_JSON, - InvalidOid); + expr->expr, JS_FORMAT_JSON, + InvalidOid, false); JsonReturning *returning; if (expr->output) @@ -4162,3 +4194,458 @@ transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), NULL, returning, false, false, expr->location); } + +/* + * Transform a JSON PASSING clause. + */ +static void +transformJsonPassingArgs(ParseState *pstate, char *constructName, + JsonFormatType format, List *args, + List **passing_values, List **passing_names) +{ + ListCell *lc; + + *passing_values = NIL; + *passing_names = NIL; + + foreach(lc, args) + { + JsonArgument *arg = castNode(JsonArgument, lfirst(lc)); + Node *expr = transformJsonValueExpr(pstate, constructName, + arg->val, format, + InvalidOid, true); + + assign_expr_collations(pstate, expr); + + *passing_values = lappend(*passing_values, expr); + *passing_names = lappend(*passing_names, makeString(arg->name)); + } +} + +/* + * Transform a JSON BEHAVIOR clause. + */ +static JsonBehavior * +transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior, + JsonBehaviorType default_behavior) +{ + JsonBehaviorType behavior_type = default_behavior; + Node *default_expr = NULL; + + if (behavior) + { + behavior_type = behavior->btype; + if (behavior_type == JSON_BEHAVIOR_DEFAULT) + default_expr = transformExprRecurse(pstate, behavior->default_expr); + } + return makeJsonBehavior(behavior_type, default_expr); +} + +/* + * Common code for JSON_VALUE, JSON_QUERY, JSON_EXISTS transformation + * into a JsonExpr node. + */ +static JsonExpr * +transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = makeNode(JsonExpr); + Node *pathspec; + JsonFormatType format; + char *constructName; + + if (func->common->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("JSON_TABLE path name is not allowed here"), + parser_errposition(pstate, func->location))); + + jsexpr->location = func->location; + jsexpr->op = func->op; + switch (jsexpr->op) + { + case JSON_VALUE_OP: + constructName = "JSON_VALUE()"; + break; + case JSON_QUERY_OP: + constructName = "JSON_QUERY()"; + break; + case JSON_EXISTS_OP: + constructName = "JSON_EXISTS()"; + break; + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); + break; + } + jsexpr->formatted_expr = transformJsonValueExpr(pstate, constructName, + func->common->expr, + JS_FORMAT_DEFAULT, + InvalidOid, false); + + assign_expr_collations(pstate, jsexpr->formatted_expr); + + /* format is determined by context item type */ + format = exprType(jsexpr->formatted_expr) == JSONBOID ? JS_FORMAT_JSONB : JS_FORMAT_JSON; + + jsexpr->result_coercion = NULL; + jsexpr->omit_quotes = false; + + jsexpr->format = func->common->expr->format; + + pathspec = transformExprRecurse(pstate, func->common->pathspec); + + jsexpr->path_spec = + coerce_to_target_type(pstate, pathspec, exprType(pathspec), + JSONPATHOID, -1, + COERCION_EXPLICIT, COERCE_IMPLICIT_CAST, + exprLocation(pathspec)); + if (!jsexpr->path_spec) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("JSON path expression must be type %s, not type %s", + "jsonpath", format_type_be(exprType(pathspec))), + parser_errposition(pstate, exprLocation(pathspec)))); + + /* transform and coerce to json[b] passing arguments */ + transformJsonPassingArgs(pstate, constructName, format, + func->common->passing, + &jsexpr->passing_values, + &jsexpr->passing_names); + + if (func->op != JSON_EXISTS_OP) + jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, + JSON_BEHAVIOR_NULL); + + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + func->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : + JSON_BEHAVIOR_NULL); + + return jsexpr; +} + +/* + * Assign default JSON returning type from the specified format or from + * the context item type. + */ +static void +assignDefaultJsonReturningType(Node *context_item, JsonFormat *context_format, + JsonReturning *ret) +{ + bool is_jsonb; + + ret->format = copyObject(context_format); + + if (ret->format->format_type == JS_FORMAT_DEFAULT) + is_jsonb = exprType(context_item) == JSONBOID; + else + is_jsonb = ret->format->format_type == JS_FORMAT_JSONB; + + ret->typid = is_jsonb ? JSONBOID : JSONOID; + ret->typmod = -1; +} + +/* + * Try to coerce expression to the output type or + * use json_populate_type() for composite, array and domain types or + * use coercion via I/O. + */ +static JsonCoercion * +coerceJsonExpr(ParseState *pstate, Node *expr, const JsonReturning *returning) +{ + char typtype; + JsonCoercion *coercion = makeNode(JsonCoercion); + + coercion->expr = coerceJsonFuncExpr(pstate, expr, returning, false); + + if (coercion->expr) + { + if (coercion->expr == expr) + coercion->expr = NULL; + + return coercion; + } + + typtype = get_typtype(returning->typid); + + if (returning->typid == RECORDOID || + typtype == TYPTYPE_COMPOSITE || + typtype == TYPTYPE_DOMAIN || + type_is_array(returning->typid)) + coercion->via_populate = true; + else + coercion->via_io = true; + + return coercion; +} + +/* + * Transform a JSON output clause of JSON_VALUE and JSON_QUERY. + */ +static void +transformJsonFuncExprOutput(ParseState *pstate, JsonFuncExpr *func, + JsonExpr *jsexpr) +{ + Node *expr = jsexpr->formatted_expr; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + /* JSON_VALUE returns text by default */ + if (func->op == JSON_VALUE_OP && !OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = TEXTOID; + jsexpr->returning->typmod = -1; + } + + if (OidIsValid(jsexpr->returning->typid)) + { + JsonReturning ret; + + if (func->op == JSON_VALUE_OP && + jsexpr->returning->typid != JSONOID && + jsexpr->returning->typid != JSONBOID) + { + /* Forced coercion via I/O for JSON_VALUE for non-JSON types */ + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = NULL; + jsexpr->result_coercion->via_io = true; + return; + } + + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, &ret); + + if (ret.typid != jsexpr->returning->typid || + ret.typmod != jsexpr->returning->typmod) + { + Node *placeholder = makeCaseTestExpr(expr); + + Assert(((CaseTestExpr *) placeholder)->typeId == ret.typid); + Assert(((CaseTestExpr *) placeholder)->typeMod == ret.typmod); + + jsexpr->result_coercion = coerceJsonExpr(pstate, placeholder, + jsexpr->returning); + } + } + else + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, + jsexpr->returning); +} + +/* + * Coerce an expression in JSON DEFAULT behavior to the target output type. + */ +static Node * +coerceDefaultJsonExpr(ParseState *pstate, JsonExpr *jsexpr, Node *defexpr) +{ + int location; + Oid exprtype; + + if (!defexpr) + return NULL; + + exprtype = exprType(defexpr); + location = exprLocation(defexpr); + + if (location < 0) + location = jsexpr->location; + + defexpr = coerce_to_target_type(pstate, + defexpr, + exprtype, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!defexpr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast DEFAULT expression type %s to %s", + format_type_be(exprtype), + format_type_be(jsexpr->returning->typid)), + parser_errposition(pstate, location))); + + return defexpr; +} + +/* + * Initialize SQL/JSON item coercion from the SQL type "typid" to the target + * "returning" type. + */ +static JsonCoercion * +initJsonItemCoercion(ParseState *pstate, Oid typid, + const JsonReturning *returning) +{ + Node *expr; + + if (typid == UNKNOWNOID) + { + expr = (Node *) makeNullConst(UNKNOWNOID, -1, InvalidOid); + } + else + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + + placeholder->typeId = typid; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + expr = (Node *) placeholder; + } + + return coerceJsonExpr(pstate, expr, returning); +} + +static void +initJsonItemCoercions(ParseState *pstate, JsonItemCoercions *coercions, + const JsonReturning *returning, Oid contextItemTypeId) +{ + struct + { + JsonCoercion **coercion; + Oid typid; + } *p, + coercionTypids[] = + { + {&coercions->null, UNKNOWNOID}, + {&coercions->string, TEXTOID}, + {&coercions->numeric, NUMERICOID}, + {&coercions->boolean, BOOLOID}, + {&coercions->date, DATEOID}, + {&coercions->time, TIMEOID}, + {&coercions->timetz, TIMETZOID}, + {&coercions->timestamp, TIMESTAMPOID}, + {&coercions->timestamptz, TIMESTAMPTZOID}, + {&coercions->composite, contextItemTypeId}, + {NULL, InvalidOid} + }; + + for (p = coercionTypids; p->coercion; p++) + *p->coercion = initJsonItemCoercion(pstate, p->typid, returning); +} + +/* + * Transform JSON_VALUE, JSON_QUERY, JSON_EXISTS functions into a JsonExpr node. + */ +static Node * +transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = transformJsonExprCommon(pstate, func); + const char *func_name = NULL; + Node *contextItemExpr = jsexpr->formatted_expr; + + /* + * Disallow FORMAT specification in the RETURNING clause of JSON_EXISTS() + * and JSON_VALUE(). + */ + if (func->output && + (func->op == JSON_VALUE_OP || func->op == JSON_EXISTS_OP)) + { + JsonFormat *format = func->output->returning->format; + + if(format->format_type != JS_FORMAT_DEFAULT || + format->encoding != JS_ENC_DEFAULT) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot specify FORMAT in RETURNING clause of %s", + func->op == JSON_VALUE_OP ? "JSON_VALUE()" : + "JSON_EXISTS()"), + parser_errposition(pstate, format->location))); + } + + switch (func->op) + { + case JSON_VALUE_OP: + func_name = "JSON_VALUE"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->coercions = makeNode(JsonItemCoercions); + initJsonItemCoercions(pstate, jsexpr->coercions, jsexpr->returning, + exprType(contextItemExpr)); + + break; + + case JSON_QUERY_OP: + func_name = "JSON_QUERY"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->wrapper = func->wrapper; + jsexpr->omit_quotes = func->omit_quotes; + + break; + + case JSON_EXISTS_OP: + func_name = "JSON_EXISTS"; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + if (!OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = BOOLOID; + jsexpr->returning->typmod = -1; + } + else if (jsexpr->returning->typid != BOOLOID) + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + int location = exprLocation((Node *) jsexpr); + + placeholder->typeId = BOOLOID; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = + coerce_to_target_type(pstate, (Node *) placeholder, BOOLOID, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!jsexpr->result_coercion->expr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(BOOLOID), + format_type_be(jsexpr->returning->typid)), + parser_coercion_errposition(pstate, location, (Node *) jsexpr))); + + if (jsexpr->result_coercion->expr == (Node *) placeholder) + jsexpr->result_coercion->expr = NULL; + } + break; + } + + if (exprType(contextItemExpr) != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("%s() is not yet implemented for the json type", func_name), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + return (Node *) jsexpr; +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 520d4f2a23..4f94fc69d6 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1979,6 +1979,21 @@ FigureColnameInternal(Node *node, char **name) /* make JSON_ARRAYAGG act like a regular function */ *name = "json_arrayagg"; return 2; + case T_JsonFuncExpr: + /* make SQL/JSON functions act like a regular function */ + switch (((JsonFuncExpr *) node)->op) + { + case JSON_QUERY_OP: + *name = "json_query"; + return 2; + case JSON_VALUE_OP: + *name = "json_value"; + return 2; + case JSON_EXISTS_OP: + *name = "json_exists"; + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index e27ea8ef97..188b5594e0 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -4426,6 +4426,50 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, } } +/* + * Parses the datetime format string in 'fmt_str' and returns true if it + * contains a timezone specifier, false if not. + */ +bool +datetime_format_has_tz(const char *fmt_str) +{ + bool incache; + int fmt_len = strlen(fmt_str); + int result; + FormatNode *format; + + if (fmt_len > DCH_CACHE_SIZE) + { + /* + * Allocate new memory if format picture is bigger than static cache + * and do not use cache (call parser always) + */ + incache = false; + + format = (FormatNode *) palloc((fmt_len + 1) * sizeof(FormatNode)); + + parse_format(format, fmt_str, DCH_keywords, + DCH_suff, DCH_index, DCH_FLAG, NULL); + } + else + { + /* + * Use cache buffers + */ + DCHCacheEntry *ent = DCH_cache_fetch(fmt_str, false); + + incache = true; + format = ent->format; + } + + result = DCH_datetime_type(format); + + if (!incache) + pfree(format); + + return result & DCH_ZONED; +} + /* * do_to_timestamp: shared code for to_timestamp and to_date * diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index 06ba409e64..d2b4da8ec8 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -2156,3 +2156,65 @@ jsonb_float8(PG_FUNCTION_ARGS) PG_RETURN_DATUM(retValue); } + +/* + * Construct an empty array jsonb. + */ +Jsonb * +JsonbMakeEmptyArray(void) +{ + JsonbValue jbv; + + jbv.type = jbvArray; + jbv.val.array.elems = NULL; + jbv.val.array.nElems = 0; + jbv.val.array.rawScalar = false; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Construct an empty object jsonb. + */ +Jsonb * +JsonbMakeEmptyObject(void) +{ + JsonbValue jbv; + + jbv.type = jbvObject; + jbv.val.object.pairs = NULL; + jbv.val.object.nPairs = 0; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Convert jsonb to a C-string stripping quotes from scalar strings. + */ +char * +JsonbUnquote(Jsonb *jb) +{ + if (JB_ROOT_IS_SCALAR(jb)) + { + JsonbValue v; + + (void) JsonbExtractScalar(&jb->root, &v); + + if (v.type == jbvString) + return pnstrdup(v.val.string.val, v.val.string.len); + else if (v.type == jbvBool) + return pstrdup(v.val.boolean ? "true" : "false"); + else if (v.type == jbvNumeric) + return DatumGetCString(DirectFunctionCall1(numeric_out, + PointerGetDatum(v.val.numeric))); + else if (v.type == jbvNull) + return pstrdup("null"); + else + { + elog(ERROR, "unrecognized jsonb value type %d", v.type); + return NULL; + } + } + else + return JsonbToCString(NULL, &jb->root, VARSIZE(jb)); +} diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index 612bbf06a3..fca72de558 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -265,6 +265,7 @@ typedef struct PopulateArrayContext int *dims; /* dimensions */ int *sizes; /* current dimension counters */ int ndims; /* number of dimensions */ + Node *escontext; /* For soft-error capture */ } PopulateArrayContext; /* state for populate_array_json() */ @@ -442,12 +443,13 @@ static void JsValueToJsObject(JsValue *jsv, JsObject *jso); static Datum populate_composite(CompositeIOData *io, Oid typid, const char *colname, MemoryContext mcxt, HeapTupleHeader defaultval, JsValue *jsv, bool isnull); -static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv); +static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext); static void prepare_column_cache(ColumnIOData *column, Oid typid, int32 typmod, MemoryContext mcxt, bool need_scalar); static Datum populate_record_field(ColumnIOData *col, Oid typid, int32 typmod, const char *colname, MemoryContext mcxt, Datum defaultval, - JsValue *jsv, bool *isnull); + JsValue *jsv, bool *isnull, Node *escontext); static RecordIOData *allocate_record_info(MemoryContext mcxt, int ncolumns); static bool JsObjectGetField(JsObject *obj, char *field, JsValue *jsv); static void populate_recordset_record(PopulateRecordsetState *state, JsObject *obj); @@ -459,7 +461,8 @@ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims); static void populate_array_check_dimension(PopulateArrayContext *ctx, int ndim); static void populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv); static Datum populate_array(ArrayIOData *aio, const char *colname, - MemoryContext mcxt, JsValue *jsv); + MemoryContext mcxt, JsValue *jsv, Node *escontext, + bool *isnull); static Datum populate_domain(DomainIOData *io, Oid typid, const char *colname, MemoryContext mcxt, JsValue *jsv, bool isnull); @@ -2484,12 +2487,12 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) if (ndim <= 0) { if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the value of key \"%s\".", ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"))); } @@ -2506,13 +2509,13 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) appendStringInfo(&indices, "[%d]", ctx->sizes[i]); if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s of key \"%s\".", indices.data, ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s.", @@ -2520,7 +2523,11 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) } } -/* set the number of dimensions of the populated array when it becomes known */ +/* + * Set the number of dimensions of the populated array when it becomes known. + * + * Returns without doing anything if the input (ndims) is erratic. + */ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) { @@ -2531,6 +2538,10 @@ populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) if (ndims <= 0) populate_array_report_expected_array(ctx, ndims); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + ctx->ndims = ndims; ctx->dims = palloc(sizeof(int) * ndims); ctx->sizes = palloc0(sizeof(int) * ndims); @@ -2548,12 +2559,16 @@ populate_array_check_dimension(PopulateArrayContext *ctx, int ndim) if (ctx->dims[ndim] == -1) ctx->dims[ndim] = dim; /* assign dimension if not yet known */ else if (ctx->dims[ndim] != dim) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("malformed JSON array"), errdetail("Multidimensional arrays must have " "sub-arrays with matching dimensions."))); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + /* reset the current array dimension size counter */ ctx->sizes[ndim] = 0; @@ -2573,7 +2588,10 @@ populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv) ctx->aio->element_type, ctx->aio->element_typmod, NULL, ctx->mcxt, PointerGetDatum(NULL), - jsv, &element_isnull); + jsv, &element_isnull, ctx->escontext); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; accumArrayResult(ctx->astate, element, element_isnull, ctx->aio->element_type, ctx->acxt); @@ -2594,6 +2612,10 @@ populate_array_object_start(void *_state) else if (ndim < state->ctx->ndims) populate_array_report_expected_array(state->ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2611,6 +2633,10 @@ populate_array_array_end(void *_state) if (ndim < ctx->ndims) populate_array_check_dimension(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2686,6 +2712,10 @@ populate_array_scalar(void *_state, char *token, JsonTokenType tokentype) else if (ndim < ctx->ndims) populate_array_report_expected_array(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + if (ndim == ctx->ndims) { /* remember the scalar element token */ @@ -2715,7 +2745,13 @@ populate_array_json(PopulateArrayContext *ctx, char *json, int len) sem.array_element_end = populate_array_element_end; sem.scalar = populate_array_scalar; - pg_parse_json_or_ereport(state.lex, &sem); + if (!pg_parse_json_or_errsave(state.lex, &sem, ctx->escontext)) + { + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + pfree(state.lex); + return; + } /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2740,10 +2776,15 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ check_stack_depth(); - if (jbv->type != jbvBinary || !JsonContainerIsArray(jbc)) + if (jbv->type != jbvBinary || + !JsonContainerIsArray(jbc) || + JsonContainerIsScalar(jbc)) + { populate_array_report_expected_array(ctx, ndim - 1); - - Assert(!JsonContainerIsScalar(jbc)); + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + return; + } it = JsonbIteratorInit(jbc); @@ -2762,7 +2803,12 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ (tok == WJB_ELEM && (val.type != jbvBinary || !JsonContainerIsArray(val.val.binary.data))))) + { populate_array_assign_ndims(ctx, ndim); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + } jsv.is_json = false; jsv.val.jsonb = &val; @@ -2780,6 +2826,9 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ { /* populate child sub-array */ populate_array_dim_jsonb(ctx, &val, ndim + 1); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2797,12 +2846,18 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ Assert(tok == WJB_DONE && !it); } -/* recursively populate an array from json/jsonb */ +/* + * Recursively populate an array from json/jsonb + * + * *isnull is set to true if an error is reported during parsing. + */ static Datum populate_array(ArrayIOData *aio, const char *colname, MemoryContext mcxt, - JsValue *jsv) + JsValue *jsv, + Node *escontext, + bool *isnull) { PopulateArrayContext ctx; Datum result; @@ -2817,6 +2872,7 @@ populate_array(ArrayIOData *aio, ctx.ndims = 0; /* unknown yet */ ctx.dims = NULL; ctx.sizes = NULL; + ctx.escontext = escontext; if (jsv->is_json) populate_array_json(&ctx, jsv->val.json.str, @@ -2825,7 +2881,16 @@ populate_array(ArrayIOData *aio, else { populate_array_dim_jsonb(&ctx, jsv->val.jsonb, 1); - ctx.dims[0] = ctx.sizes[0]; + /* Nothing to do on an error. */ + if (!SOFT_ERROR_OCCURRED(ctx.escontext)) + ctx.dims[0] = ctx.sizes[0]; + } + + /* Nothing to return if an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx.escontext)) + { + *isnull = true; + return (Datum) 0; } Assert(ctx.ndims > 0); @@ -2842,6 +2907,7 @@ populate_array(ArrayIOData *aio, pfree(ctx.sizes); pfree(lbs); + *isnull = false; return result; } @@ -2957,7 +3023,8 @@ populate_composite(CompositeIOData *io, /* populate non-null scalar value from json/jsonb value */ static Datum -populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) +populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext) { Datum res; char *str = NULL; @@ -3028,7 +3095,11 @@ populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) elog(ERROR, "unrecognized jsonb type: %d", (int) jbv->type); } - res = InputFunctionCall(&io->typiofunc, str, io->typioparam, typmod); + if (!InputFunctionCallSafe(&io->typiofunc, str, io->typioparam, typmod, + escontext, &res)) + { + res = (Datum) 0; + } /* free temporary buffer */ if (str != json) @@ -3054,7 +3125,7 @@ populate_domain(DomainIOData *io, res = populate_record_field(io->base_io, io->base_typid, io->base_typmod, colname, mcxt, PointerGetDatum(NULL), - jsv, &isnull); + jsv, &isnull, NULL); Assert(!isnull); } @@ -3159,7 +3230,8 @@ populate_record_field(ColumnIOData *col, MemoryContext mcxt, Datum defaultval, JsValue *jsv, - bool *isnull) + bool *isnull, + Node *escontext) { TypeCat typcat; @@ -3192,10 +3264,12 @@ populate_record_field(ColumnIOData *col, switch (typcat) { case TYPECAT_SCALAR: - return populate_scalar(&col->scalar_io, typid, typmod, jsv); + return populate_scalar(&col->scalar_io, typid, typmod, jsv, + escontext); case TYPECAT_ARRAY: - return populate_array(&col->io.array, colname, mcxt, jsv); + return populate_array(&col->io.array, colname, mcxt, jsv, + escontext, isnull); case TYPECAT_COMPOSITE: case TYPECAT_COMPOSITE_DOMAIN: @@ -3216,6 +3290,53 @@ populate_record_field(ColumnIOData *col, } } +/* recursively populate specified type from a json/jsonb value */ +Datum +json_populate_type(Datum json_val, Oid json_type, Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext) +{ + JsValue jsv = {0}; + JsonbValue jbv; + + jsv.is_json = json_type == JSONOID; + + if (*isnull) + { + if (jsv.is_json) + jsv.val.json.str = NULL; + else + jsv.val.jsonb = NULL; + } + else if (jsv.is_json) + { + text *json = DatumGetTextPP(json_val); + + jsv.val.json.str = VARDATA_ANY(json); + jsv.val.json.len = VARSIZE_ANY_EXHDR(json); + jsv.val.json.type = JSON_TOKEN_INVALID; /* not used in + * populate_composite() */ + } + else + { + Jsonb *jsonb = DatumGetJsonbP(json_val); + + jsv.val.jsonb = &jbv; + + /* fill binary jsonb value pointing to jb */ + jbv.type = jbvBinary; + jbv.val.binary.data = &jsonb->root; + jbv.val.binary.len = VARSIZE(jsonb) - VARHDRSZ; + } + + if (!*cache) + *cache = MemoryContextAllocZero(mcxt, sizeof(ColumnIOData)); + + return populate_record_field(*cache, typid, typmod, NULL, mcxt, + PointerGetDatum(NULL), &jsv, isnull, + escontext); +} + static RecordIOData * allocate_record_info(MemoryContext mcxt, int ncolumns) { @@ -3357,7 +3478,8 @@ populate_record(TupleDesc tupdesc, mcxt, nulls[i] ? (Datum) 0 : values[i], &field, - &nulls[i]); + &nulls[i], + NULL); } res = heap_form_tuple(tupdesc, values, nulls); diff --git a/src/backend/utils/adt/jsonpath.c b/src/backend/utils/adt/jsonpath.c index 7891fde310..5194d0d91f 100644 --- a/src/backend/utils/adt/jsonpath.c +++ b/src/backend/utils/adt/jsonpath.c @@ -68,7 +68,9 @@ #include "libpq/pqformat.h" #include "nodes/miscnodes.h" #include "miscadmin.h" +#include "nodes/nodeFuncs.h" #include "utils/builtins.h" +#include "utils/formatting.h" #include "utils/json.h" #include "utils/jsonpath.h" @@ -1109,3 +1111,256 @@ jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, return true; } + +/* SQL/JSON datatype status: */ +typedef enum JsonPathDatatypeStatus +{ + jpdsNonDateTime, /* null, bool, numeric, string, array, object */ + jpdsUnknownDateTime, /* unknown datetime type */ + jpdsDateTimeZoned, /* timetz, timestamptz */ + jpdsDateTimeNonZoned /* time, timestamp, date */ +} JsonPathDatatypeStatus; + +/* Context for jspIsMutableWalker() */ +typedef struct JsonPathMutableContext +{ + List *varnames; /* list of variable names */ + List *varexprs; /* list of variable expressions */ + JsonPathDatatypeStatus current; /* status of @ item */ + bool lax; /* jsonpath is lax or strict */ + bool mutable; /* resulting mutability status */ +} JsonPathMutableContext; + +/* + * Recursive walker for jspIsMutable() + */ +static JsonPathDatatypeStatus +jspIsMutableWalker(JsonPathItem *jpi, JsonPathMutableContext *cxt) +{ + JsonPathItem next; + JsonPathDatatypeStatus status = jpdsNonDateTime; + + while (!cxt->mutable) + { + JsonPathItem arg; + JsonPathDatatypeStatus leftStatus; + JsonPathDatatypeStatus rightStatus; + + switch (jpi->type) + { + case jpiRoot: + Assert(status == jpdsNonDateTime); + break; + + case jpiCurrent: + Assert(status == jpdsNonDateTime); + status = cxt->current; + break; + + case jpiFilter: + { + JsonPathDatatypeStatus prevStatus = cxt->current; + + cxt->current = status; + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + + cxt->current = prevStatus; + break; + } + + case jpiVariable: + { + int32 len; + const char *name = jspGetString(jpi, &len); + ListCell *lc1; + ListCell *lc2; + + Assert(status == jpdsNonDateTime); + + forboth(lc1, cxt->varnames, lc2, cxt->varexprs) + { + String *varname = lfirst_node(String, lc1); + Node *varexpr = lfirst(lc2); + + if (strncmp(varname->sval, name, len)) + continue; + + switch (exprType(varexpr)) + { + case DATEOID: + case TIMEOID: + case TIMESTAMPOID: + status = jpdsDateTimeNonZoned; + break; + + case TIMETZOID: + case TIMESTAMPTZOID: + status = jpdsDateTimeZoned; + break; + + default: + status = jpdsNonDateTime; + break; + } + + break; + } + break; + } + + case jpiEqual: + case jpiNotEqual: + case jpiLess: + case jpiGreater: + case jpiLessOrEqual: + case jpiGreaterOrEqual: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + leftStatus = jspIsMutableWalker(&arg, cxt); + + jspGetRightArg(jpi, &arg); + rightStatus = jspIsMutableWalker(&arg, cxt); + + /* + * Comparison of datetime type with different timezone status + * is mutable. + */ + if (leftStatus != jpdsNonDateTime && + rightStatus != jpdsNonDateTime && + (leftStatus == jpdsUnknownDateTime || + rightStatus == jpdsUnknownDateTime || + leftStatus != rightStatus)) + cxt->mutable = true; + break; + + case jpiNot: + case jpiIsUnknown: + case jpiExists: + case jpiPlus: + case jpiMinus: + Assert(status == jpdsNonDateTime); + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiAnd: + case jpiOr: + case jpiAdd: + case jpiSub: + case jpiMul: + case jpiDiv: + case jpiMod: + case jpiStartsWith: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + jspGetRightArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiIndexArray: + for (int i = 0; i < jpi->content.array.nelems; i++) + { + JsonPathItem from; + JsonPathItem to; + + if (jspGetArraySubscript(jpi, &from, &to, i)) + jspIsMutableWalker(&to, cxt); + + jspIsMutableWalker(&from, cxt); + } + /* FALLTHROUGH */ + + case jpiAnyArray: + if (!cxt->lax) + status = jpdsNonDateTime; + break; + + case jpiAny: + if (jpi->content.anybounds.first > 0) + status = jpdsNonDateTime; + break; + + case jpiDatetime: + if (jpi->content.arg) + { + char *template; + + jspGetArg(jpi, &arg); + if (arg.type != jpiString) + { + status = jpdsNonDateTime; + break; /* there will be runtime error */ + } + + template = jspGetString(&arg, NULL); + if (datetime_format_has_tz(template)) + status = jpdsDateTimeZoned; + else + status = jpdsDateTimeNonZoned; + } + else + { + status = jpdsUnknownDateTime; + } + break; + + case jpiLikeRegex: + Assert(status == jpdsNonDateTime); + jspInitByBuffer(&arg, jpi->base, jpi->content.like_regex.expr); + jspIsMutableWalker(&arg, cxt); + break; + + /* literals */ + case jpiNull: + case jpiString: + case jpiNumeric: + case jpiBool: + /* accessors */ + case jpiKey: + case jpiAnyKey: + /* special items */ + case jpiSubscript: + case jpiLast: + /* item methods */ + case jpiType: + case jpiSize: + case jpiAbs: + case jpiFloor: + case jpiCeiling: + case jpiDouble: + case jpiKeyValue: + status = jpdsNonDateTime; + break; + } + + if (!jspGetNext(jpi, &next)) + break; + + jpi = &next; + } + + return status; +} + +/* + * Check whether jsonpath expression is immutable or not. + */ +bool +jspIsMutable(JsonPath *path, List *varnames, List *varexprs) +{ + JsonPathMutableContext cxt; + JsonPathItem jpi; + + cxt.varnames = varnames; + cxt.varexprs = varexprs; + cxt.current = jpdsNonDateTime; + cxt.lax = (path->header & JSONPATH_LAX) != 0; + cxt.mutable = false; + + jspInit(&jpi, path); + jspIsMutableWalker(&jpi, &cxt); + + return cxt.mutable; +} diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index 2d0599b4aa..eded22e194 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -87,12 +87,16 @@ typedef struct JsonBaseObjectInfo int id; } JsonBaseObjectInfo; +typedef int (*JsonPathVarCallback) (void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); + /* * Context of jsonpath execution. */ typedef struct JsonPathExecContext { - Jsonb *vars; /* variables to substitute into jsonpath */ + void *vars; /* variables to substitute into jsonpath */ + JsonPathVarCallback getVar; JsonbValue *root; /* for $ evaluation */ JsonbValue *current; /* for @ evaluation */ JsonBaseObjectInfo baseObject; /* "base object" for .keyvalue() @@ -174,7 +178,8 @@ typedef JsonPathBool (*JsonPathPredicateCallback) (JsonPathItem *jsp, void *param); typedef Numeric (*BinaryArithmFunc) (Numeric num1, Numeric num2, bool *error); -static JsonPathExecResult executeJsonPath(JsonPath *path, Jsonb *vars, +static JsonPathExecResult executeJsonPath(JsonPath *path, void *vars, + JsonPathVarCallback getVar, Jsonb *json, bool throwErrors, JsonValueList *result, bool useTz); static JsonPathExecResult executeItem(JsonPathExecContext *cxt, @@ -225,8 +230,13 @@ static JsonPathExecResult appendBoolResult(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonValueList *found, JsonPathBool res); static void getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, JsonbValue *value); +static int GetJsonPathVar(void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); static void getJsonPathVariable(JsonPathExecContext *cxt, - JsonPathItem *variable, Jsonb *vars, JsonbValue *value); + JsonPathItem *variable, JsonbValue *value); +static int getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, + int varNameLen, JsonbValue *val, + JsonbValue *baseObject); static int JsonbArraySize(JsonbValue *jb); static JsonPathBool executeComparison(JsonPathItem *cmp, JsonbValue *lv, JsonbValue *rv, void *p); @@ -284,7 +294,8 @@ jsonb_path_exists_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - res = executeJsonPath(jp, vars, jb, !silent, NULL, tz); + res = executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, NULL, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -339,7 +350,8 @@ jsonb_path_match_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -417,7 +429,8 @@ jsonb_path_query_internal(FunctionCallInfo fcinfo, bool tz) vars = PG_GETARG_JSONB_P_COPY(2); silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); funcctx->user_fctx = JsonValueListGetList(&found); @@ -464,7 +477,8 @@ jsonb_path_query_array_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_RETURN_JSONB_P(JsonbValueToJsonb(wrapItemsInArray(&found))); } @@ -495,7 +509,8 @@ jsonb_path_query_first_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); if (JsonValueListLength(&found) >= 1) PG_RETURN_JSONB_P(JsonbValueToJsonb(JsonValueListHead(&found))); @@ -537,8 +552,9 @@ jsonb_path_query_first_tz(PG_FUNCTION_ARGS) * In other case it tries to find all the satisfied result items. */ static JsonPathExecResult -executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, - JsonValueList *result, bool useTz) +executeJsonPath(JsonPath *path, void *vars, JsonPathVarCallback getVar, + Jsonb *json, bool throwErrors, JsonValueList *result, + bool useTz) { JsonPathExecContext cxt; JsonPathExecResult res; @@ -550,22 +566,16 @@ executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, if (!JsonbExtractScalar(&json->root, &jbv)) JsonbInitBinary(&jbv, json); - if (vars && !JsonContainerIsObject(&vars->root)) - { - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"vars\" argument is not an object"), - errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); - } - cxt.vars = vars; + cxt.getVar = getVar; cxt.laxMode = (path->header & JSONPATH_LAX) != 0; cxt.ignoreStructuralErrors = cxt.laxMode; cxt.root = &jbv; cxt.current = &jbv; cxt.baseObject.jbc = NULL; cxt.baseObject.id = 0; - cxt.lastGeneratedObjectId = vars ? 2 : 1; + /* 1 + number of base objects in vars */ + cxt.lastGeneratedObjectId = 1 + getVar(vars, NULL, 0, NULL, NULL); cxt.innermostArraySize = -1; cxt.throwErrors = throwErrors; cxt.useTz = useTz; @@ -2108,54 +2118,118 @@ getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, &value->val.string.len); break; case jpiVariable: - getJsonPathVariable(cxt, item, cxt->vars, value); + getJsonPathVariable(cxt, item, value); return; default: elog(ERROR, "unexpected jsonpath item type"); } } +/* + * Returns the computed value of a JSON path variable with given name. + */ +static int +GetJsonPathVar(void *cxt, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject) +{ + JsonPathVariable *var = NULL; + List *vars = cxt; + ListCell *lc; + int id = 1; + + if (!varName) + return list_length(vars); + + foreach(lc, vars) + { + JsonPathVariable *curvar = lfirst(lc); + + if (!strncmp(curvar->name, varName, varNameLen)) + { + var = curvar; + break; + } + + id++; + } + + if (!var) + return -1; + + if (var->isnull) + { + val->type = jbvNull; + return 0; + } + + JsonItemFromDatum(var->value, var->typid, var->typmod, val); + + *baseObject = *val; + return id; +} + /* * Get the value of variable passed to jsonpath executor */ static void getJsonPathVariable(JsonPathExecContext *cxt, JsonPathItem *variable, - Jsonb *vars, JsonbValue *value) + JsonbValue *value) { char *varName; int varNameLength; + JsonbValue baseObject; + int baseObjectId; + + Assert(variable->type == jpiVariable); + varName = jspGetString(variable, &varNameLength); + + if (!cxt->vars || + (baseObjectId = cxt->getVar(cxt->vars, varName, varNameLength, value, + &baseObject)) < 0) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("could not find jsonpath variable \"%s\"", + pnstrdup(varName, varNameLength)))); + + if (baseObjectId > 0) + setBaseObject(cxt, &baseObject, baseObjectId); +} + +static int +getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, int varNameLength, + JsonbValue *value, JsonbValue *baseObject) +{ + Jsonb *vars = varsJsonb; JsonbValue tmp; JsonbValue *v; - if (!vars) + if (!varName) { - value->type = jbvNull; - return; + if (vars && !JsonContainerIsObject(&vars->root)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"vars\" argument is not an object"), + errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); + } + + return vars ? 1 : 0; /* count of base objects */ } - Assert(variable->type == jpiVariable); - varName = jspGetString(variable, &varNameLength); tmp.type = jbvString; tmp.val.string.val = varName; tmp.val.string.len = varNameLength; v = findJsonbValueFromContainer(&vars->root, JB_FOBJECT, &tmp); - if (v) - { - *value = *v; - pfree(v); - } - else - { - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("could not find jsonpath variable \"%s\"", - pnstrdup(varName, varNameLength)))); - } + if (!v) + return -1; - JsonbInitBinary(&tmp, vars); - setBaseObject(cxt, &tmp, 1); + *value = *v; + pfree(v); + + JsonbInitBinary(baseObject, vars); + return 1; } /**************** Support functions for JsonPath execution *****************/ @@ -2812,3 +2886,240 @@ compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, return DatumGetInt32(DirectFunctionCall2(cmpfunc, val1, val2)); } + +/* Executor-callable JSON_EXISTS implementation */ +bool +JsonPathExists(Datum jb, JsonPath *jp, List *vars, bool *error) +{ + JsonPathExecResult res = executeJsonPath(jp, vars, GetJsonPathVar, + DatumGetJsonbP(jb), !error, NULL, + true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + *error = true; + + return res == jperOk; +} + +/* Executor-callable JSON_QUERY implementation */ +Datum +JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, bool *empty, + bool *error, List *vars) +{ + JsonbValue *first; + bool wrap; + JsonValueList found = {0}; + JsonPathExecResult res PG_USED_FOR_ASSERTS_ONLY; + int count; + + res = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + { + *error = true; + *empty = false; + return (Datum) 0; + } + + count = JsonValueListLength(&found); + + first = count ? JsonValueListHead(&found) : NULL; + + if (!first) + wrap = false; + else if (wrapper == JSW_NONE) + wrap = false; + else if (wrapper == JSW_UNCONDITIONAL) + wrap = true; + else if (wrapper == JSW_CONDITIONAL) + wrap = count > 1 || + IsAJsonbScalar(first) || + (first->type == jbvBinary && + JsonContainerIsScalar(first->val.binary.data)); + else + { + elog(ERROR, "unrecognized json wrapper %d", wrapper); + wrap = false; + } + + if (wrap) + return JsonbPGetDatum(JsonbValueToJsonb(wrapItemsInArray(&found))); + + if (count > 1) + { + if (error) + { + *error = true; + return (Datum) 0; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_QUERY should return singleton item without wrapper"), + errhint("Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array."))); + } + + if (first) + return JsonbPGetDatum(JsonbValueToJsonb(first)); + + *empty = true; + return PointerGetDatum(NULL); +} + +/* Executor-callable JSON_VALUE implementation */ +JsonbValue * +JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars) +{ + JsonbValue *res; + JsonValueList found = {0}; + JsonPathExecResult jper PG_USED_FOR_ASSERTS_ONLY; + int count; + + jper = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(jper)); + + if (error && jperIsError(jper)) + { + *error = true; + *empty = false; + return NULL; + } + + count = JsonValueListLength(&found); + + *empty = !count; + + if (*empty) + return NULL; + + if (count > 1) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + res = JsonValueListHead(&found); + + if (res->type == jbvBinary && + JsonContainerIsScalar(res->val.binary.data)) + JsonbExtractScalar(res->val.binary.data, res); + + if (!IsAJsonbScalar(res)) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_SCALAR_REQUIRED), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + if (res->type == jbvNull) + return NULL; + + return res; +} + +static void +JsonbValueInitNumericDatum(JsonbValue *jbv, Datum num) +{ + jbv->type = jbvNumeric; + jbv->val.numeric = DatumGetNumeric(num); +} + +void +JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) +{ + switch (typid) + { + case BOOLOID: + res->type = jbvBool; + res->val.boolean = DatumGetBool(val); + break; + case NUMERICOID: + JsonbValueInitNumericDatum(res, val); + break; + case INT2OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int2_numeric, val)); + break; + case INT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int4_numeric, val)); + break; + case INT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int8_numeric, val)); + break; + case FLOAT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float4_numeric, val)); + break; + case FLOAT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float8_numeric, val)); + break; + case TEXTOID: + case VARCHAROID: + res->type = jbvString; + res->val.string.val = VARDATA_ANY(val); + res->val.string.len = VARSIZE_ANY_EXHDR(val); + break; + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + res->type = jbvDatetime; + res->val.datetime.value = val; + res->val.datetime.typid = typid; + res->val.datetime.typmod = typmod; + res->val.datetime.tz = 0; + break; + case JSONBOID: + { + JsonbValue *jbv = res; + Jsonb *jb = DatumGetJsonbP(val); + + if (JsonContainerIsScalar(&jb->root)) + { + bool result PG_USED_FOR_ASSERTS_ONLY; + + result = JsonbExtractScalar(&jb->root, jbv); + Assert(result); + } + else + JsonbInitBinary(jbv, jb); + break; + } + case JSONOID: + { + text *txt = DatumGetTextP(val); + char *str = text_to_cstring(txt); + Jsonb *jb; + + jb = DatumGetJsonbP(DirectFunctionCall1(jsonb_in, + CStringGetDatum(str))); + pfree(str); + + JsonItemFromDatum(JsonbPGetDatum(jb), JSONBOID, -1, res); + break; + } + default: + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); + } +} diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d1b03d6cb2..d1ec418ccf 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -476,6 +476,8 @@ static void get_const_expr(Const *constval, deparse_context *context, int showtype); static void get_const_collation(Const *constval, deparse_context *context); static void get_json_format(JsonFormat *format, StringInfo buf); +static void get_json_returning(JsonReturning *returning, StringInfo buf, + bool json_format_by_default); static void get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, bool showimplicit); static void get_json_constructor_options(JsonConstructorExpr *ctor, @@ -518,6 +520,8 @@ static char *generate_qualified_type_name(Oid typid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); +static void get_json_path_spec(Node *path_spec, deparse_context *context, + bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8279,6 +8283,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_WindowFunc: case T_FuncExpr: case T_JsonConstructorExpr: + case T_JsonExpr: /* function-like: name(..) or name[..] */ return true; @@ -8450,6 +8455,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_GroupingFunc: /* own parentheses */ case T_WindowFunc: /* own parentheses */ case T_CaseExpr: /* other separators */ + case T_JsonExpr: /* own parentheses */ return true; default: return false; @@ -8565,6 +8571,65 @@ get_rule_expr_paren(Node *node, deparse_context *context, appendStringInfoChar(context->buf, ')'); } +static void +get_json_behavior(JsonBehavior *behavior, deparse_context *context, + const char *on) +{ + /* + * The order of array elements must correspond to the order of + * JsonBehaviorType members. + */ + const char *behavior_names[] = + { + " NULL", + " ERROR", + " EMPTY", + " TRUE", + " FALSE", + " UNKNOWN", + " EMPTY ARRAY", + " EMPTY OBJECT", + " DEFAULT " + }; + + if ((int) behavior->btype < 0 || behavior->btype >= lengthof(behavior_names)) + elog(ERROR, "invalid json behavior type: %d", behavior->btype); + + appendStringInfoString(context->buf, behavior_names[behavior->btype]); + + if (behavior->btype == JSON_BEHAVIOR_DEFAULT) + get_rule_expr(behavior->default_expr, context, false); + + appendStringInfo(context->buf, " ON %s", on); +} + +/* + * get_json_expr_options + * + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + */ +static void +get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, + JsonBehaviorType default_behavior) +{ + if (jsexpr->op == JSON_QUERY_OP) + { + if (jsexpr->wrapper == JSW_CONDITIONAL) + appendStringInfo(context->buf, " WITH CONDITIONAL WRAPPER"); + else if (jsexpr->wrapper == JSW_UNCONDITIONAL) + appendStringInfo(context->buf, " WITH UNCONDITIONAL WRAPPER"); + + if (jsexpr->omit_quotes) + appendStringInfo(context->buf, " OMIT QUOTES"); + } + + if (jsexpr->op != JSON_EXISTS_OP && + jsexpr->on_empty->btype != default_behavior) + get_json_behavior(jsexpr->on_empty, context, "EMPTY"); + + if (jsexpr->on_error->btype != default_behavior) + get_json_behavior(jsexpr->on_error, context, "ERROR"); +} /* ---------- * get_rule_expr - Parse back an expression @@ -9724,6 +9789,7 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonValueExpr: { JsonValueExpr *jve = (JsonValueExpr *) node; @@ -9773,6 +9839,63 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + appendStringInfoString(buf, "JSON_QUERY("); + break; + case JSON_VALUE_OP: + appendStringInfoString(buf, "JSON_VALUE("); + break; + case JSON_EXISTS_OP: + appendStringInfoString(buf, "JSON_EXISTS("); + break; + } + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_json_path_spec(jexpr->path_spec, context, showimplicit); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoString(buf, " PASSING "); + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + get_rule_expr((Node *) lfirst(lc2), context, showimplicit); + appendStringInfo(buf, " AS %s", + ((String *) lfirst_node(String, lc1))->sval); + } + } + + if (jexpr->op != JSON_EXISTS_OP || + jexpr->returning->typid != BOOLOID) + get_json_returning(jexpr->returning, context->buf, + jexpr->op == JSON_QUERY_OP); + + get_json_expr_options(jexpr, context, + jexpr->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : JSON_BEHAVIOR_NULL); + + appendStringInfoString(buf, ")"); + } + break; + case T_List: { char *sep; @@ -9896,6 +10019,7 @@ looks_like_function(Node *node) case T_MinMaxExpr: case T_SQLValueFunction: case T_XmlExpr: + case T_JsonExpr: /* these are all accepted by func_expr_common_subexpr */ return true; default: @@ -10755,6 +10879,18 @@ get_const_collation(Const *constval, deparse_context *context) } } +/* + * get_json_path_spec - Parse back a JSON path specification + */ +static void +get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit) +{ + if (IsA(path_spec, Const)) + get_const_expr((Const *) path_spec, context, -1); + else + get_rule_expr(path_spec, context, showimplicit); +} + /* * get_json_format - Parse back a JsonFormat node */ diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index 048573c2bc..69fb577850 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -16,12 +16,15 @@ #include "executor/nodeAgg.h" #include "nodes/execnodes.h" +#include "nodes/miscnodes.h" /* forward references to avoid circularity */ struct ExprEvalStep; struct SubscriptingRefState; struct ScalarArrayOpExprHashTable; struct JsonConstructorExprState; +struct JsonbValue; +struct JsonExprState; /* Bits in ExprState->flags (see also execnodes.h for public flag bits): */ /* expression's interpreter has been initialized */ @@ -238,6 +241,11 @@ typedef enum ExprEvalOp EEOP_XMLEXPR, EEOP_JSON_CONSTRUCTOR, EEOP_IS_JSON, + EEOP_JSONEXPR_SKIP, + EEOP_JSONEXPR_PATH, + EEOP_JSONEXPR_BEHAVIOR, + EEOP_JSONEXPR_COERCION, + EEOP_JSONEXPR_COERCION_FINISH, EEOP_AGGREF, EEOP_GROUPING_FUNC, EEOP_WINDOW_FUNC, @@ -689,6 +697,57 @@ typedef struct ExprEvalStep JsonIsPredicate *pred; /* original expression node */ } is_json; + /* for EEOP_JSONEXPR_PATH */ + struct + { + struct JsonExprState *jsestate; + } jsonexpr; + + /* for EEOP_JSONEXPR_SKIP */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprSkip() */ + int jump_coercion; + int jump_passing_args; + } jsonexpr_skip; + + /* for EEOP_JSONEXPR_BEHAVIOR */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprBehavior() */ + int jump_onerror_expr; + int jump_onempty_expr; + int jump_coercion; + int jump_skip_coercion; + } jsonexpr_behavior; + + /* for EEOP_JSONEXPR_COERCION */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion; + + /* for EEOP_JSONEXPR_COERCION_FINISH */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion_finish; } d; } ExprEvalStep; @@ -752,6 +811,111 @@ typedef struct JsonConstructorExprState int nargs; } JsonConstructorExprState; +/* + * Information computed before evaluating a JsonExpr expression. + */ +typedef struct JsonExprPreEvalState +{ + /* value/isnull for JsonExpr.formatted_expr */ + NullableDatum formatted_expr; + + /* value/isnull for JsonExpr.pathspec */ + NullableDatum pathspec; + + /* JsonPathVariable entries for JsonExpr.passing_values */ + List *args; +} JsonExprPreEvalState; + +/* + * State for a given member of JsonItemCoercions. + */ +typedef struct JsonCoercionState +{ + /* Expression used to evaluate the coercion */ + JsonCoercion *coercion; + + /* ExprEvalStep to compute this coercion's expression */ + int jump_eval_expr; +} JsonCoercionState; + +/* + * State for evaluating the coercion for a given JSON item using one of + * the following coercions. + * + * Note that while ExecInitExprRec() for JsonItemCoercions will initialize + * ExprEvalSteps for all of the members that need it, only one will get run + * during a given evaluation of the enclosing JsonExpr depending on the type + * of the result JSON item. + */ +typedef struct JsonItemCoercionsState +{ + JsonCoercionState *null; + JsonCoercionState *string; + JsonCoercionState *numeric; + JsonCoercionState *boolean; + JsonCoercionState *date; + JsonCoercionState *time; + JsonCoercionState *timetz; + JsonCoercionState *timestamp; + JsonCoercionState *timestamptz; + JsonCoercionState *composite; +} JsonItemCoercionsState; + +/* + * State for some post-JsonExpr-evaluation processing steps that gets filled + * in JsonExpr evaluation. + */ +typedef struct JsonExprPostEvalState +{ + /* Is JSON item empty? */ + bool empty; + + /* Did JSON item evaluation cause an error? */ + bool error; + + /* Cache for json_populate_type() called for coercion in some cases */ + void *cache; + + /* + * State for evaluating a JSON item coercion. Points to one of those in + * JsonExprState.coercions chosen by ExecPrepareJsonItemCoercion(). + */ + JsonCoercionState *item_jcstate; + bool coercion_error; + bool coercion_done; + + ErrorSaveContext escontext; +} JsonExprPostEvalState; + +/* EEOP_JSONEXPR state, too big to inline */ +typedef struct JsonExprState +{ + /* original expression node */ + JsonExpr *jsexpr; + + /* + * Should errors be thrown or handled softly? Different parts of + * evaluating a given JsonExpr may require different treatment depending + * on the JsonExprOp; see ExecEvalJsonExprBehavior(). + */ + bool throw_error; + + JsonExprPreEvalState pre_eval; + JsonExprPostEvalState post_eval; + + struct + { + FmgrInfo *finfo; /* typinput function for output type */ + Oid typioparam; + } input; /* I/O info for output type */ + + /* + * Either of the following two is used by ExecEvalJsonExprCoercion() to + * apply coercion to the final result if needed. + */ + JsonCoercionState *result_jcstate; + JsonItemCoercionsState *item_jcstates; +} JsonExprState; /* functions in execExpr.c */ extern void ExprEvalPushStep(ExprState *es, const ExprEvalStep *s); @@ -805,6 +969,14 @@ extern void ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op); extern void ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull); +extern int ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op); +extern void ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, + ExprContext *econtext); extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op); extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op, ExprContext *econtext); diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index ac02247947..089d1c5750 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -17,6 +17,7 @@ #include "executor/execdesc.h" #include "fmgr.h" #include "nodes/lockoptions.h" +#include "nodes/miscnodes.h" #include "nodes/parsenodes.h" #include "utils/memutils.h" diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index cb714f4a19..584bf7001a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -129,6 +129,9 @@ typedef struct ExprState Datum *innermost_domainval; bool *innermost_domainnull; + + /* ErrorSaveContext for soft-error capture. */ + Node *escontext; } ExprState; diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 06d991b725..5e149b0266 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -111,6 +111,7 @@ extern VacuumRelation *makeVacuumRelation(RangeVar *relation, Oid oid, List *va_ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *expr, JsonFormat *format); +extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index d926713bd9..7d63a37d5f 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1727,6 +1727,12 @@ typedef enum JsonQuotes JS_QUOTES_OMIT /* OMIT QUOTES */ } JsonQuotes; +/* + * JsonPathSpec - + * representation of JSON path constant + */ +typedef char *JsonPathSpec; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1738,6 +1744,48 @@ typedef struct JsonOutput JsonReturning *returning; /* RETURNING FORMAT clause and type Oids */ } JsonOutput; +/* + * JsonArgument - + * representation of argument from JSON PASSING clause + */ +typedef struct JsonArgument +{ + NodeTag type; + JsonValueExpr *val; /* argument value expression */ + char *name; /* argument name */ +} JsonArgument; + +/* + * JsonCommon - + * representation of common syntax of functions using JSON path + */ +typedef struct JsonCommon +{ + NodeTag type; + JsonValueExpr *expr; /* context item expression */ + Node *pathspec; /* JSON path specification expression */ + char *pathname; /* path name, if any */ + List *passing; /* list of PASSING clause arguments, if any */ + int location; /* token location, or -1 if unknown */ +} JsonCommon; + +/* + * JsonFuncExpr - + * untransformed representation of JSON function expressions + */ +typedef struct JsonFuncExpr +{ + NodeTag type; + JsonExprOp op; /* expression type */ + JsonCommon *common; /* common syntax */ + JsonOutput *output; /* output clause, if specified */ + JsonBehavior *on_empty; /* ON EMPTY behavior, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + JsonWrapper wrapper; /* array wrapper behavior (JSON_QUERY only) */ + bool omit_quotes; /* omit or keep quotes? (JSON_QUERY only) */ + int location; /* token location, or -1 if unknown */ +} JsonFuncExpr; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 42adaffc5d..62345a939e 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1542,6 +1542,17 @@ typedef struct XmlExpr int location; } XmlExpr; +/* + * JsonExprOp - + * enumeration of JSON functions using JSON path + */ +typedef enum JsonExprOp +{ + JSON_VALUE_OP, /* JSON_VALUE() */ + JSON_QUERY_OP, /* JSON_QUERY() */ + JSON_EXISTS_OP /* JSON_EXISTS() */ +} JsonExprOp; + /* * JsonEncoding - * representation of JSON ENCODING clause @@ -1566,6 +1577,37 @@ typedef enum JsonFormatType * jsonb */ } JsonFormatType; +/* + * JsonBehaviorType - + * enumeration of behavior types used in JSON ON ... BEHAVIOR clause + * + * If enum members are reordered, get_json_behavior() from ruleutils.c + * must be updated accordingly. + */ +typedef enum JsonBehaviorType +{ + JSON_BEHAVIOR_NULL = 0, + JSON_BEHAVIOR_ERROR, + JSON_BEHAVIOR_EMPTY, + JSON_BEHAVIOR_TRUE, + JSON_BEHAVIOR_FALSE, + JSON_BEHAVIOR_UNKNOWN, + JSON_BEHAVIOR_EMPTY_ARRAY, + JSON_BEHAVIOR_EMPTY_OBJECT, + JSON_BEHAVIOR_DEFAULT +} JsonBehaviorType; + +/* + * JsonWrapper - + * representation of WRAPPER clause for JSON_QUERY() + */ +typedef enum JsonWrapper +{ + JSW_NONE, + JSW_CONDITIONAL, + JSW_UNCONDITIONAL, +} JsonWrapper; + /* * JsonFormat - * representation of JSON FORMAT clause @@ -1659,6 +1701,73 @@ typedef struct JsonIsPredicate int location; /* token location, or -1 if unknown */ } JsonIsPredicate; +/* + * JsonBehavior - + * representation of JSON ON ... BEHAVIOR clause + */ +typedef struct JsonBehavior +{ + NodeTag type; + JsonBehaviorType btype; /* behavior type */ + Node *default_expr; /* default expression, if any */ +} JsonBehavior; + +/* + * JsonCoercion - + * coercion from SQL/JSON item types to SQL types + */ +typedef struct JsonCoercion +{ + NodeTag type; + Node *expr; /* resulting expression coerced to target type */ + bool via_populate; /* coerce result using json_populate_type()? */ + bool via_io; /* coerce result using type input function? */ + Oid collation; /* collation for coercion via I/O or populate */ +} JsonCoercion; + +/* + * JsonItemCoercions - + * expressions for coercion from SQL/JSON item types directly to the + * output SQL type + */ +typedef struct JsonItemCoercions +{ + NodeTag type; + JsonCoercion *null; + JsonCoercion *string; + JsonCoercion *numeric; + JsonCoercion *boolean; + JsonCoercion *date; + JsonCoercion *time; + JsonCoercion *timetz; + JsonCoercion *timestamp; + JsonCoercion *timestamptz; + JsonCoercion *composite; /* arrays and objects */ +} JsonItemCoercions; + +/* + * JsonExpr - + * transformed representation of JSON_VALUE(), JSON_QUERY(), JSON_EXISTS() + */ +typedef struct JsonExpr +{ + Expr xpr; + JsonExprOp op; /* json function ID */ + Node *formatted_expr; /* formatted context item expression */ + JsonCoercion *result_coercion; /* resulting coercion to RETURNING type */ + JsonFormat *format; /* context item format (JSON/JSONB) */ + Node *path_spec; /* JSON path specification expression */ + List *passing_names; /* PASSING argument names */ + List *passing_values; /* PASSING argument values */ + JsonReturning *returning; /* RETURNING clause type/format info */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + JsonItemCoercions *coercions; /* coercions for JSON_VALUE */ + JsonWrapper wrapper; /* WRAPPER for JSON_QUERY */ + bool omit_quotes; /* KEEP/OMIT QUOTES for JSON_QUERY */ + int location; /* token location, or -1 if unknown */ +} JsonExpr; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 5984dcfa4b..0954d9fc7b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -93,6 +93,7 @@ PG_KEYWORD("commit", COMMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("committed", COMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("compression", COMPRESSION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("concurrently", CONCURRENTLY, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("conditional", CONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("configuration", CONFIGURATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("conflict", CONFLICT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("connection", CONNECTION, UNRESERVED_KEYWORD, BARE_LABEL) @@ -147,11 +148,13 @@ PG_KEYWORD("double", DOUBLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("drop", DROP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("each", EACH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("empty", EMPTY_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("event", EVENT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("except", EXCEPT, RESERVED_KEYWORD, AS_LABEL) @@ -233,10 +236,14 @@ PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_exists", JSON_EXISTS, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) @@ -302,6 +309,7 @@ PG_KEYWORD("off", OFF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("offset", OFFSET, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("oids", OIDS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("old", OLD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("omit", OMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("on", ON, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("only", ONLY, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("operator", OPERATOR, UNRESERVED_KEYWORD, BARE_LABEL) @@ -344,6 +352,7 @@ PG_KEYWORD("procedures", PROCEDURES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("publication", PUBLICATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("quotes", QUOTES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("range", RANGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("read", READ, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("real", REAL, COL_NAME_KEYWORD, BARE_LABEL) @@ -414,6 +423,7 @@ PG_KEYWORD("stdout", STDOUT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("storage", STORAGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("stored", STORED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("string", STRING_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("subscription", SUBSCRIPTION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("substring", SUBSTRING, COL_NAME_KEYWORD, BARE_LABEL) @@ -449,6 +459,7 @@ PG_KEYWORD("types", TYPES_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uescape", UESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unbounded", UNBOUNDED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uncommitted", UNCOMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("unconditional", UNCONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unencrypted", UNENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("union", UNION, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("unique", UNIQUE, RESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/formatting.h b/src/include/utils/formatting.h index 0cad3a2709..d4ca0f42ff 100644 --- a/src/include/utils/formatting.h +++ b/src/include/utils/formatting.h @@ -17,7 +17,6 @@ #ifndef _FORMATTING_H_ #define _FORMATTING_H_ - extern char *str_tolower(const char *buff, size_t nbytes, Oid collid); extern char *str_toupper(const char *buff, size_t nbytes, Oid collid); extern char *str_initcap(const char *buff, size_t nbytes, Oid collid); @@ -29,5 +28,6 @@ extern char *asc_initcap(const char *buff, size_t nbytes); extern Datum parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, Oid *typid, int32 *typmod, int *tz, struct Node *escontext); +extern bool datetime_format_has_tz(const char *fmt_str); #endif diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index f928e6142a..e628d4fdd0 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -422,6 +422,9 @@ extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, int estimated_len); +extern Jsonb *JsonbMakeEmptyArray(void); +extern Jsonb *JsonbMakeEmptyObject(void); +extern char *JsonbUnquote(Jsonb *jb); extern bool JsonbExtractScalar(JsonbContainer *jbc, JsonbValue *res); extern const char *JsonbTypeName(JsonbValue *val); diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index 973e51f958..e89e3fb385 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -15,6 +15,7 @@ #define JSONFUNCS_H #include "common/jsonapi.h" +#include "nodes/nodes.h" #include "utils/jsonb.h" /* @@ -87,5 +88,9 @@ extern Datum to_json_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid); extern Datum to_jsonb_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid); +extern Datum json_populate_type(Datum json_val, Oid json_type, + Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext); #endif diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index f0181e045f..4dae78a98c 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -16,7 +16,9 @@ #include "fmgr.h" #include "nodes/pg_list.h" +#include "nodes/primnodes.h" #include "utils/jsonb.h" +#include "utils/jsonfuncs.h" typedef struct { @@ -184,6 +186,7 @@ extern bool jspGetBool(JsonPathItem *v); extern char *jspGetString(JsonPathItem *v, int32 *len); extern bool jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, int i); +extern bool jspIsMutable(JsonPath *path, List *varnames, List *varexprs); extern const char *jspOperationName(JsonPathItemType type); @@ -261,4 +264,28 @@ extern bool jspConvertRegexFlags(uint32 xflags, int *result, struct Node *escontext); +/* + * Evaluation of jsonpath + */ + +/* External variable passed into jsonpath. */ +typedef struct JsonPathVariable +{ + char *name; + Oid typid; + int32 typmod; + Datum value; + bool isnull; +} JsonPathVariable; + +/* SQL/JSON item */ +extern void JsonItemFromDatum(Datum val, Oid typid, int32 typmod, + JsonbValue *res); + +extern bool JsonPathExists(Datum jb, JsonPath *path, List *vars, bool *error); +extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, + bool *empty, bool *error, List *vars); +extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, + bool *error, List *vars); + #endif diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer index 435c139ec2..b2aa44f36d 100644 --- a/src/interfaces/ecpg/preproc/ecpg.trailer +++ b/src/interfaces/ecpg/preproc/ecpg.trailer @@ -651,6 +651,34 @@ var_type: simple_type $$.type_index = mm_strdup("-1"); $$.type_sizeof = NULL; } + | STRING_P + { + if (INFORMIX_MODE) + { + /* In Informix mode, "string" is automatically a typedef */ + $$.type_enum = ECPGt_string; + $$.type_str = mm_strdup("char"); + $$.type_dimension = mm_strdup("-1"); + $$.type_index = mm_strdup("-1"); + $$.type_sizeof = NULL; + } + else + { + /* Otherwise, legal only if user typedef'ed it */ + struct typedefs *this = get_typedef("string", false); + + $$.type_str = (this->type->type_enum == ECPGt_varchar || this->type->type_enum == ECPGt_bytea) ? EMPTY : mm_strdup(this->name); + $$.type_enum = this->type->type_enum; + $$.type_dimension = this->type->type_dimension; + $$.type_index = this->type->type_index; + if (this->type->type_sizeof && strlen(this->type->type_sizeof) != 0) + $$.type_sizeof = this->type->type_sizeof; + else + $$.type_sizeof = cat_str(3, mm_strdup("sizeof("), mm_strdup(this->name), mm_strdup(")")); + + struct_member_list[struct_level] = ECPGstruct_member_dup(this->struct_member_list); + } + } | INTERVAL ecpg_interval { $$.type_enum = ECPGt_interval; diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out new file mode 100644 index 0000000000..8b87580752 --- /dev/null +++ b/src/test/regress/expected/json_sqljson.out @@ -0,0 +1,18 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); +ERROR: JSON_EXISTS() is not yet implemented for the json type +LINE 1: SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_VALUE +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); +ERROR: JSON_VALUE() is not yet implemented for the json type +LINE 1: SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_QUERY +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); +ERROR: JSON_QUERY() is not yet implemented for the json type +LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out new file mode 100644 index 0000000000..22f8679917 --- /dev/null +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -0,0 +1,1042 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL::jsonb, '$'); + json_exists +------------- + +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb 'null', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_EXISTS(jsonb 'null', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{}', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + json_exists +------------- + f +(1 row) + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + json_exists +------------- + t +(1 row) + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); + json_exists +------------- + 1 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); + json_exists +------------- + 0 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); + json_exists +------------- + true +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +ERROR: cannot cast type boolean to jsonb +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +ERROR: cannot cast type boolean to real +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_EXISTS() +LINE 1: ...CT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSO... + ^ +-- JSON_VALUE +SELECT JSON_VALUE(NULL::jsonb, '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$'); + json_value +------------ + true +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + json_value +------------ + t +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); + json_value +------------ + 123 +(1 row) + +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); +ERROR: SQL/JSON item cannot be cast to target type +SELECT JSON_VALUE(jsonb '1.23', '$'); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "1.23" +SELECT JSON_VALUE(jsonb '"aaa"', '$'); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); + json_value +------------ + aa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "aaa" +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); + json_value +------------ + 111 +(1 row) + +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + ?column? +------------ + 03-01-2017 +(1 row) + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '[]', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '{}', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '1', '$.a'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); + json_value +------------ + error +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 3 +(1 row) + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); + json_value +------------ + 0 +(1 row) + +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: " " +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 5 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_VALUE() +LINE 1: ...CT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSO... + ^ +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + x | y +---+---- + 0 | -2 + 1 | 2 + 2 | -1 +(3 rows) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); + json_value +------------ + (1,2) +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + json_value +------------ + (1,2) +(1 row) + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); + json_value +-------------------------- + Tue Feb 20 18:34:56 2018 +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- JSON_QUERY +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + json_query | json_query | json_query | json_query | json_query +--------------------+--------------------+--------------------+----------------------+---------------------- + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, null, "2"] | [1, null, "2"] | [1, null, "2"] | [[1, null, "2"]] | [[1, null, "2"]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] +(6 rows) + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + unspec | without | with cond | with uncond | with +--------------------+--------------------+---------------------+----------------------+---------------------- + | | | | + | | | | + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, 2, 3] | [1, 2, 3] | [1, 2, 3] | [[1, 2, 3]] | [[1, 2, 3]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] + | | [1, "2", null, [3]] | [1, "2", null, [3]] | [1, "2", null, [3]] +(9 rows) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + json_query +------------ + \x616161 +(1 row) + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTE... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTE... + ^ +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_QUERY should return singleton item without wrapper +HINT: Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array. +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); + json_query +------------ + [1, +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); +ERROR: expected JSON array +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + x | y | list +---+---+-------------- + 0 | 0 | [] + 0 | 1 | [1] + 0 | 2 | [1, 2] + 0 | 3 | [1, 2, 3] + 0 | 4 | [1, 2, 3, 4] + 1 | 0 | [] + 1 | 1 | [1] + 1 | 2 | [1, 2] + 1 | 3 | [1, 2, 3] + 1 | 4 | [1, 2, 3, 4] + 2 | 0 | [] + 2 | 1 | [] + 2 | 2 | [2] + 2 | 3 | [2, 3] + 2 | 4 | [2, 3, 4] + 3 | 0 | [] + 3 | 1 | [] + 3 | 2 | [] + 3 | 3 | [3] + 3 | 4 | [3, 4] + 4 | 0 | [] + 4 | 1 | [] + 4 | 2 | [] + 4 | 3 | [] + 4 | 4 | [4] +(25 rows) + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); + json_query +----------------------------------------------------- + (1,aaa,"[1, ""2"", {}]","{""x"": [1, ""2"", {}]}",) +(1 row) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); + unnest +------------------------ + {"a": 1, "b": ["foo"]} + {"a": 2, "c": {}} + 123 +(3 rows) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); + json_query +-------------- + {1,2,NULL,3} +(1 row) + +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); + json_query +------------ + 1 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- Test constraints +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); +\d test_jsonb_constraints + Table "public.test_jsonb_constraints" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+-------------------------------------------------------------------------------- + js | text | | | + i | integer | | | + x | jsonb | | | JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +Check constraints: + "test_jsonb_constraint1" CHECK (js IS JSON) + "test_jsonb_constraint2" CHECK (JSON_EXISTS(js::jsonb, '$."a"' PASSING i + 5 AS int, i::text AS txt, ARRAY[1, 2, 3] AS arr)) + "test_jsonb_constraint3" CHECK (JSON_VALUE(js::jsonb, '$."a"' RETURNING integer DEFAULT ('12'::text || i)::integer ON EMPTY ERROR ON ERROR) > i) + "test_jsonb_constraint4" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb) + "test_jsonb_constraint5" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C")) + "test_jsonb_constraint6" CHECK (JSON_EXISTS(js::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2) + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + check_clause +-------------------------------------------------------------------------------------------------------------------------- + ((JSON_EXISTS((js)::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2)) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C"))) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb)) + ((JSON_VALUE((js)::jsonb, '$."a"' RETURNING integer DEFAULT (('12'::text || i))::integer ON EMPTY ERROR ON ERROR) > i)) + ((js IS JSON)) + (JSON_EXISTS((js)::jsonb, '$."a"' PASSING (i + 5) AS int, (i)::text AS txt, ARRAY[1, 2, 3] AS arr)) +(6 rows) + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +WHERE constraint_name LIKE 'test_jsonb_constraint%'; +ERROR: syntax error at or near "WHERE" +LINE 1: WHERE constraint_name LIKE 'test_jsonb_constraint%'; + ^ +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint1" +DETAIL: Failing row contains (, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains (1, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('[]'); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ([], null, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ({"b": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint3" +DETAIL: Failing row contains ({"a": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint5" +DETAIL: Failing row contains ({"a": 7}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint4" +DETAIL: Failing row contains ({"a": 10}, 1, [1, 2]). +DROP TABLE test_jsonb_constraints; +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index cf46fa3359..cb3230f4dd 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -103,7 +103,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo # ---------- # Another group of parallel tests (JSON related) # ---------- -test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson +test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson json_sqljson jsonb_sqljson # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql new file mode 100644 index 0000000000..4f30fa46b9 --- /dev/null +++ b/src/test/regress/sql/json_sqljson.sql @@ -0,0 +1,11 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + +-- JSON_QUERY + +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql new file mode 100644 index 0000000000..b8a6148b7b --- /dev/null +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -0,0 +1,327 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL::jsonb, '$'); + +SELECT JSON_EXISTS(jsonb '[]', '$'); +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + +SELECT JSON_EXISTS(jsonb '1', '$'); +SELECT JSON_EXISTS(jsonb 'null', '$'); +SELECT JSON_EXISTS(jsonb '[]', '$'); + +SELECT JSON_EXISTS(jsonb '1', '$.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_EXISTS(jsonb 'null', '$.a'); +SELECT JSON_EXISTS(jsonb '[]', '$.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); +SELECT JSON_EXISTS(jsonb '{}', '$.a'); +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSON); -- RETURNING FORMAT not allowed + + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL::jsonb, '$'); + +SELECT JSON_VALUE(jsonb 'null', '$'); +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + +SELECT JSON_VALUE(jsonb 'true', '$'); +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + +SELECT JSON_VALUE(jsonb '123', '$'); +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1.23', '$'); +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '"aaa"', '$'); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); + +SELECT JSON_VALUE(jsonb '[]', '$'); +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '{}', '$'); +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1', '$.a'); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed + +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- JSON_QUERY + +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); + +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); + +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); + +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- Test constraints + +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); + +\d test_jsonb_constraints + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; +WHERE constraint_name LIKE 'test_jsonb_constraint%'; + +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +INSERT INTO test_jsonb_constraints VALUES ('[]'); +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); + +DROP TABLE test_jsonb_constraints; + +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index d251fb9a91..6e4c026492 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1254,14 +1254,24 @@ JsonArrayAgg JsonArrayConstructor JsonArrayQueryConstructor JsonBaseObjectInfo +JsonBehavior +JsonBehaviorType +JsonCoercion +JsonCoercionState +JsonCommon JsonConstructorExpr JsonConstructorExprState JsonConstructorType JsonEncoding +JsonExpr +JsonExprOp +JsonExprState JsonFormat JsonFormatType JsonHashEntry JsonIsPredicate +JsonItemCoercions +JsonItemCoercionsState JsonIterateStringValuesAction JsonKeyValue JsonLexContext @@ -1294,6 +1304,10 @@ JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback JsonPathString +JsonPathVarCallback +JsonPathVariable +JsonPathVariableEvalContext +JsonQuotes JsonReturning JsonSemAction JsonSerializeExpr -- 2.35.3
v4-0002-Don-t-include-CaseTestExpr-in-JsonValueExpr.forma.patch (application/octet-stream, 6.2K) download From e2e69da761ba69557c8ed0f3908c370755d56292 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Fri, 7 Jul 2023 20:21:58 +0900 Subject: [PATCH v4 2/7] Don't include CaseTestExpr in JsonValueExpr.formatted_expr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CaseTestExpr is normally (ab)used as a placeholder expression to represent the source value for computing an expression when the expression that provides the source value is computed independently. A CaseTestExpr is currently put into JsonValueExpr.formatted_expr as placeholder for the result of evaluating JsonValueExpr.raw_expr, which in turn is evaluated separately. Though, there's no need for this indirection if raw_expr itself can be embedded into formatted_expr and evaluated as part of evaluating the latter, so this commit makes it so. JsonValueExpr.raw_expr is no longer evaluated by ExecInterpExpr(), eval_const_exprs_mutator(), etc. and only used for displaying the original "unformatted" expression. Reviewed-by: Álvaro Herrera <alvherre@alvh.no-ip.org> Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- src/backend/executor/execExpr.c | 17 ++--------------- src/backend/nodes/makefuncs.c | 4 ++++ src/backend/optimizer/util/clauses.c | 23 +++++------------------ src/backend/parser/parse_expr.c | 12 +++++++----- src/include/nodes/primnodes.h | 5 ++++- 5 files changed, 22 insertions(+), 39 deletions(-) diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index e6e616865c..bf3a08c5f0 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -2294,21 +2294,8 @@ ExecInitExprRec(Expr *node, ExprState *state, { JsonValueExpr *jve = (JsonValueExpr *) node; - ExecInitExprRec(jve->raw_expr, state, resv, resnull); - - if (jve->formatted_expr) - { - Datum *innermost_caseval = state->innermost_caseval; - bool *innermost_isnull = state->innermost_casenull; - - state->innermost_caseval = resv; - state->innermost_casenull = resnull; - - ExecInitExprRec(jve->formatted_expr, state, resv, resnull); - - state->innermost_caseval = innermost_caseval; - state->innermost_casenull = innermost_isnull; - } + Assert(jve->formatted_expr != NULL); + ExecInitExprRec(jve->formatted_expr, state, resv, resnull); break; } diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 39e1884cf4..c6c310d253 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -853,6 +853,10 @@ makeJsonValueExpr(Expr *expr, JsonFormat *format) JsonValueExpr *jve = makeNode(JsonValueExpr); jve->raw_expr = expr; + + /* + * Set after checking the format, if needed, in transformJsonValueExpr(). + */ jve->formatted_expr = NULL; jve->format = format; diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 7f453b04f8..da258968b8 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -2827,25 +2827,12 @@ eval_const_expressions_mutator(Node *node, case T_JsonValueExpr: { JsonValueExpr *jve = (JsonValueExpr *) node; - Node *raw; + Node *formatted; - raw = eval_const_expressions_mutator((Node *) jve->raw_expr, - context); - if (raw && IsA(raw, Const)) - { - Node *formatted; - Node *save_case_val = context->case_val; - - context->case_val = raw; - - formatted = eval_const_expressions_mutator((Node *) jve->formatted_expr, - context); - - context->case_val = save_case_val; - - if (formatted && IsA(formatted, Const)) - return formatted; - } + formatted = eval_const_expressions_mutator((Node *) jve->formatted_expr, + context); + if (formatted && IsA(formatted, Const)) + return formatted; break; } diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 5bf790cf0f..20cbb0a8e7 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -3268,11 +3268,8 @@ transformJsonValueExpr(ParseState *pstate, char *constructName, if (format != JS_FORMAT_DEFAULT) { Oid targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; - Node *orig = makeCaseTestExpr(expr); Node *coerced; - expr = orig; - if (exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), @@ -3310,7 +3307,7 @@ transformJsonValueExpr(ParseState *pstate, char *constructName, coerced = (Node *) fexpr; } - if (coerced == orig) + if (coerced == expr) expr = rawexpr; else { @@ -3636,6 +3633,11 @@ transformJsonArrayQueryConstructor(ParseState *pstate, colref->location = ctor->location; agg->arg = makeJsonValueExpr((Expr *) colref, ctor->format); + /* + * No formatting necessary, so set formatted_expr to be the same as + * raw_expr. + */ + agg->arg->formatted_expr = agg->arg->raw_expr; agg->absent_on_null = ctor->absent_on_null; agg->constructor = makeNode(JsonAggConstructor); agg->constructor->agg_order = NIL; @@ -3900,7 +3902,7 @@ transformJsonParseArg(ParseState *pstate, Node *jsexpr, JsonFormat *format, { JsonValueExpr *jve; - expr = makeCaseTestExpr(raw_expr); + expr = raw_expr; expr = makeJsonByteaToTextConversion(expr, format, exprLocation(expr)); *exprtype = TEXTOID; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 792a743f72..442d675600 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1593,12 +1593,15 @@ typedef struct JsonReturning /* * JsonValueExpr - * representation of JSON value expression (expr [FORMAT JsonFormat]) + * + * Note that raw_expr is only there for displaying and is not evaluated by + * ExecInterpExpr() and eval_const_exprs_mutator(). */ typedef struct JsonValueExpr { NodeTag type; Expr *raw_expr; /* raw expression */ - Expr *formatted_expr; /* formatted expression or NULL */ + Expr *formatted_expr; /* formatted expression */ JsonFormat *format; /* FORMAT clause, if specified */ } JsonValueExpr; -- 2.35.3
v4-0001-Pass-constructName-to-transformJsonValueExpr.patch (application/octet-stream, 3.6K) download From 52afabf6c26da0ebd37e2a81e7a89b8a2a903b78 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Fri, 7 Jul 2023 12:08:58 +0900 Subject: [PATCH v4 1/7] Pass constructName to transformJsonValueExpr() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows it to pass to coerce_to_specific_type() the actual name corresponding to the specific JSON_* function expression being transformed, instead of the currently hardcoded string. Reviewed-by: Álvaro Herrera <alvherre@alvh.no-ip.org> Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- src/backend/parser/parse_expr.c | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 346fd272b6..5bf790cf0f 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -3222,8 +3222,8 @@ makeCaseTestExpr(Node *expr) * default format otherwise. */ static Node * -transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, - JsonFormatType default_format) +transformJsonValueExpr(ParseState *pstate, char *constructName, + JsonValueExpr *ve, JsonFormatType default_format) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3233,12 +3233,8 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, char typcategory; bool typispreferred; - /* - * Using JSON_VALUE here is slightly bogus: perhaps we need to be passed a - * JsonConstructorType so that we can use one of JSON_OBJECTAGG, etc. - */ if (exprType(expr) == UNKNOWNOID) - expr = coerce_to_specific_type(pstate, expr, TEXTOID, "JSON_VALUE"); + expr = coerce_to_specific_type(pstate, expr, TEXTOID, constructName); rawexpr = expr; exprtype = exprType(expr); @@ -3588,7 +3584,8 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) { JsonKeyValue *kv = castNode(JsonKeyValue, lfirst(lc)); Node *key = transformExprRecurse(pstate, (Node *) kv->key); - Node *val = transformJsonValueExpr(pstate, kv->value, + Node *val = transformJsonValueExpr(pstate, "JSON_OBJECT()", + kv->value, JS_FORMAT_DEFAULT); args = lappend(args, key); @@ -3768,7 +3765,9 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) Oid aggtype; key = transformExprRecurse(pstate, (Node *) agg->arg->key); - val = transformJsonValueExpr(pstate, agg->arg->value, JS_FORMAT_DEFAULT); + val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", + agg->arg->value, + JS_FORMAT_DEFAULT); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3824,7 +3823,9 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) Oid aggfnoid; Oid aggtype; - arg = transformJsonValueExpr(pstate, agg->arg, JS_FORMAT_DEFAULT); + arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", + agg->arg, + JS_FORMAT_DEFAULT); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3870,7 +3871,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) foreach(lc, ctor->exprs) { JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); - Node *val = transformJsonValueExpr(pstate, jsval, + Node *val = transformJsonValueExpr(pstate, "JSON_ARRAY()", + jsval, JS_FORMAT_DEFAULT); args = lappend(args, val); -- 2.35.3
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-12 13:23  Amit Langote <amitlangote09@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-07-12 13:23 UTC (permalink / raw)
  To: Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: pgsql-hackers; Andrew Dunstan <andrew@dunslane.net>

On Wed, Jul 12, 2023 at 6:41 PM Amit Langote <amitlangote09@gmail.com> wrote:
> On Mon, Jul 10, 2023 at 11:52 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
> > I forgot to add:
>
> Thanks for the review of these.
>
> > * 0001 looks an obvious improvement.  You could just push it now, to
> > avoid carrying it forward anymore.  I would just put the constructName
> > ahead of value expr in the argument list, though.
>
> Sure, that makes sense.
>
> > * 0002: I have no idea what this is (though I probably should).  I would
> > also push it right away -- if anything, so that we figure out sooner
> > that it was actually needed in the first place.  Or maybe you just need
> > the right test cases?
>
> Hmm, I don't think having or not having CaseTestExpr makes a
> difference to the result of evaluating JsonValueExpr.format_expr, so
> there are no test cases to prove one way or the other.
>
> After staring at this again for a while, I think I figured out why the
> CaseTestExpr might have been put there in the first place.  It seems
> to have to do with the fact that JsonValueExpr.raw_expr is currently
> evaluated independently of JsonValueExpr.formatted_expr and the
> CaseTestExpr propagates the result of the former to the evaluation of
> the latter.  Actually, formatted_expr is effectively
> formatting_function(<result-of-raw_expr>), so if we put raw_expr
> itself into formatted_expr such that it is evaluated as part of
> evaluating formatted_expr, then there is no need for the CaseTestExpr
> as the propagator for raw_expr's result.
>
> I've expanded the commit message to mention the details.
>
> I'll push these tomorrow.

I updated it to make the code in makeJsonConstructorExpr() that *does*
need to use a CaseTestExpr a bit more readable.  Also, updated the
comment above CaseTestExpr to mention this instance of its usage.

> On Mon, Jul 10, 2023 at 11:47 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
> > On 2023-Jul-10, Amit Langote wrote:
> > > > I'm not in love with the fact that JSON and JSONB have pretty much
> > > > parallel type categorizing functionality. It seems entirely artificial.
> > > > Maybe this didn't matter when these were contained inside each .c file
> > > > and nobody else had to deal with that, but I think it's not good to make
> > > > this an exported concept.  Is it possible to do away with that?  I mean,
> > > > reduce both to a single categorization enum, and a single categorization
> > > > API.  Here you have to cast the enum value to int in order to make
> > > > ExecInitExprRec work, and that seems a bit lame; moreso when the
> > > > "is_jsonb" is determined separately (cf. ExecEvalJsonConstructor)
> > >
> > > OK, I agree that a unified categorizing API might be better.  I'll
> > > look at making this better.  Btw, does src/include/common/jsonapi.h
> > > look like an appropriate place for that?
> >
> > Hmm, that header is frontend-available, and the type-category appears to
> > be backend-only, so maybe no.  Perhaps jsonfuncs.h is more apropos?
> > execExpr.c is already dealing with array internals, so having to deal
> > with json internals doesn't seem completely out of place.
>
> OK, attached 0003 does it like that.  Essentially, I decided to only
> keep JsonTypeCategory and json_categorize_type(), with some
> modifications to accommodate the callers in jsonb.c.
>
> > > > In the 2023 standard, JSON_SCALAR is just
> > > >
> > > > <JSON scalar> ::= JSON_SCALAR <left paren> <value expression> <right paren>
> > > >
> > > > but we seem to have added a <JSON output format> clause to it.  Should
> > > > we really?
> > >
> > > Hmm, I am not seeing <JSON output format> in the rule for JSON_SCALAR,
> >
> > Agh, yeah, I confused myself, sorry.
> >
> > > Per what I wrote above, the grammar for JSON() and JSON_SCALAR() does
> > > not allow specifying the FORMAT clause.  Though considering what you
> > > wrote, the RETURNING clause does appear to be an extension to the
> > > standard's spec.
> >
> > Hmm, I see that <JSON output clause> (which is RETURNING plus optional
> > FORMAT) appears included in JSON_OBJECT, JSON_ARRAY, JSON_QUERY,
> > JSON_SERIALIZE, JSON_OBJECTAGG, JSON_ARRAYAGG.  It's not necessarily a
> > bad thing to have it in other places, but we should consider it
> > carefully.  Do we really want/need it in JSON() and JSON_SCALAR()?
>
> I thought that removing that support breaks JSON_TABLE() or something
> but it doesn't, so maybe we can do without the extension if there's no
> particular reason it's there in the first place.  Maybe Andrew (cc'd)
> remembers why he decided in [1] to (re-) add the RETURNING clause to
> JSON() and JSON_SCALAR()?
>
> Updated patches, with 0003 being a new refactoring patch, are
> attached.  Patches 0004~ contain a few updates around JsonValueExpr.
> Specifically, I removed the case for T_JsonValueExpr in
> transformExprRecurse(), because I realized that JsonValueExpr
> expressions never appear embedded in other expressions.  That allowed
> me to get rid of some needless refactoring around
> transformJsonValueExpr() in the patch that adds JSON_VALUE() etc.

I noticed that 0003 was giving some warnings, which is fixed in the
attached updated set of patches.

--
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

Attachments: download all (7 files, .tar.gz)

v5-0007-Claim-SQL-standard-compliance-for-SQL-JSON-featur.patch (application/octet-stream, 2.3K) download From 2f110cb98d9920c8d855942717ef3df2330045e0 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:16:39 +0900 Subject: [PATCH v5 7/7] Claim SQL standard compliance for SQL/JSON features Discussion: https://postgr.es/m/d03d809c-d0fb-fd6a-1476-d6dc18ec940e@dunslane.net --- src/backend/catalog/sql_features.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt index b33065d7bf..b379c71df9 100644 --- a/src/backend/catalog/sql_features.txt +++ b/src/backend/catalog/sql_features.txt @@ -548,15 +548,15 @@ T811 Basic SQL/JSON constructor functions YES T812 SQL/JSON: JSON_OBJECTAGG YES T813 SQL/JSON: JSON_ARRAYAGG with ORDER BY YES T814 Colon in JSON_OBJECT or JSON_OBJECTAGG YES -T821 Basic SQL/JSON query operators NO +T821 Basic SQL/JSON query operators YES T822 SQL/JSON: IS JSON WITH UNIQUE KEYS predicate YES -T823 SQL/JSON: PASSING clause NO -T824 JSON_TABLE: specific PLAN clause NO -T825 SQL/JSON: ON EMPTY and ON ERROR clauses NO -T826 General value expression in ON ERROR or ON EMPTY clauses NO -T827 JSON_TABLE: sibling NESTED COLUMNS clauses NO -T828 JSON_QUERY NO -T829 JSON_QUERY: array wrapper options NO +T823 SQL/JSON: PASSING clause YES +T824 JSON_TABLE: specific PLAN clause YES +T825 SQL/JSON: ON EMPTY and ON ERROR clauses YES +T826 General value expression in ON ERROR or ON EMPTY clauses YES +T827 JSON_TABLE: sibling NESTED COLUMNS clauses YES +T828 JSON_QUERY YES +T829 JSON_QUERY: array wrapper options YES T830 Enforcing unique keys in SQL/JSON constructor functions YES T831 SQL/JSON path language: strict mode YES T832 SQL/JSON path language: item method YES @@ -565,7 +565,7 @@ T834 SQL/JSON path language: wildcard member accessor YES T835 SQL/JSON path language: filter expressions YES T836 SQL/JSON path language: starts with predicate YES T837 SQL/JSON path language: regex_like predicate YES -T838 JSON_TABLE: PLAN DEFAULT clause NO +T838 JSON_TABLE: PLAN DEFAULT clause YES T839 Formatted cast of datetimes to/from character strings NO T840 Hex integer literals in SQL/JSON path language YES T851 SQL/JSON: optional keywords for default syntax YES -- 2.35.3
v5-0003-Unify-JSON-categorize-type-API-and-export-for-ext.patch (application/octet-stream, 27.5K) download From 9053356c96af83c326bc83de16a67010d975d6e3 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Tue, 11 Jul 2023 16:00:42 +0900 Subject: [PATCH v5 3/7] Unify JSON categorize type API and export for external use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This essentially removes the JsonbTypeCategory enum and jsonb_categorize_type() and integrates any jsonb-specific logic that was in jsonb_categorize_type() into json_categorize_type(), now moved to jsonfuncs.c, such that it covers the needs of the callers in both json.c and jsonb.c. The common JsonTypeCategory enum is also exported in jsonfuncs.h. json_categorize_type() has grown a new parameter named is_jsonb for callers in jsonb.c to engage the jsonb-specific behavior of json_categorize_type(). Reviewed-by: Álvaro Herrera <alvherre@alvh.no-ip.org> Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- src/backend/utils/adt/json.c | 137 ++--------------- src/backend/utils/adt/jsonb.c | 245 +++++++----------------------- src/backend/utils/adt/jsonfuncs.c | 111 ++++++++++++++ src/include/utils/jsonfuncs.h | 20 +++ src/tools/pgindent/typedefs.list | 1 - 5 files changed, 199 insertions(+), 315 deletions(-) diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index 49080e5fbf..f6bef9c148 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -19,7 +19,6 @@ #include "funcapi.h" #include "libpq/pqformat.h" #include "miscadmin.h" -#include "parser/parse_coerce.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/date.h" @@ -29,21 +28,6 @@ #include "utils/lsyscache.h" #include "utils/typcache.h" -typedef enum /* type categories for datum_to_json */ -{ - JSONTYPE_NULL, /* null, so we didn't bother to identify */ - JSONTYPE_BOOL, /* boolean (built-in types only) */ - JSONTYPE_NUMERIC, /* numeric (ditto) */ - JSONTYPE_DATE, /* we use special formatting for datetimes */ - JSONTYPE_TIMESTAMP, - JSONTYPE_TIMESTAMPTZ, - JSONTYPE_JSON, /* JSON itself (and JSONB) */ - JSONTYPE_ARRAY, /* array */ - JSONTYPE_COMPOSITE, /* composite */ - JSONTYPE_CAST, /* something with an explicit cast to JSON */ - JSONTYPE_OTHER /* all else */ -} JsonTypeCategory; - /* * Support for fast key uniqueness checking. @@ -107,9 +91,6 @@ static void array_dim_to_json(StringInfo result, int dim, int ndims, int *dims, bool use_line_feeds); static void array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds); -static void json_categorize_type(Oid typoid, - JsonTypeCategory *tcategory, - Oid *outfuncoid); static void datum_to_json(Datum val, bool is_null, StringInfo result, JsonTypeCategory tcategory, Oid outfuncoid, bool key_scalar); @@ -182,106 +163,6 @@ json_recv(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(cstring_to_text_with_len(str, nbytes)); } -/* - * Determine how we want to print values of a given type in datum_to_json. - * - * Given the datatype OID, return its JsonTypeCategory, as well as the type's - * output function OID. If the returned category is JSONTYPE_CAST, we - * return the OID of the type->JSON cast function instead. - */ -static void -json_categorize_type(Oid typoid, - JsonTypeCategory *tcategory, - Oid *outfuncoid) -{ - bool typisvarlena; - - /* Look through any domain */ - typoid = getBaseType(typoid); - - *outfuncoid = InvalidOid; - - /* - * We need to get the output function for everything except date and - * timestamp types, array and composite types, booleans, and non-builtin - * types where there's a cast to json. - */ - - switch (typoid) - { - case BOOLOID: - *tcategory = JSONTYPE_BOOL; - break; - - case INT2OID: - case INT4OID: - case INT8OID: - case FLOAT4OID: - case FLOAT8OID: - case NUMERICOID: - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - *tcategory = JSONTYPE_NUMERIC; - break; - - case DATEOID: - *tcategory = JSONTYPE_DATE; - break; - - case TIMESTAMPOID: - *tcategory = JSONTYPE_TIMESTAMP; - break; - - case TIMESTAMPTZOID: - *tcategory = JSONTYPE_TIMESTAMPTZ; - break; - - case JSONOID: - case JSONBOID: - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - *tcategory = JSONTYPE_JSON; - break; - - default: - /* Check for arrays and composites */ - if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID - || typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID) - *tcategory = JSONTYPE_ARRAY; - else if (type_is_rowtype(typoid)) /* includes RECORDOID */ - *tcategory = JSONTYPE_COMPOSITE; - else - { - /* It's probably the general case ... */ - *tcategory = JSONTYPE_OTHER; - /* but let's look for a cast to json, if it's not built-in */ - if (typoid >= FirstNormalObjectId) - { - Oid castfunc; - CoercionPathType ctype; - - ctype = find_coercion_pathway(JSONOID, typoid, - COERCION_EXPLICIT, - &castfunc); - if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc)) - { - *tcategory = JSONTYPE_CAST; - *outfuncoid = castfunc; - } - else - { - /* non builtin type with no cast */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - } - else - { - /* any other builtin type */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - } - break; - } -} - /* * Turn a Datum into JSON text, appending the string to "result". * @@ -591,7 +472,7 @@ array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds) get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign); - json_categorize_type(element_type, + json_categorize_type(element_type, false, &tcategory, &outfuncoid); deconstruct_array(v, element_type, typlen, typbyval, @@ -665,7 +546,8 @@ composite_to_json(Datum composite, StringInfo result, bool use_line_feeds) outfuncoid = InvalidOid; } else - json_categorize_type(att->atttypid, &tcategory, &outfuncoid); + json_categorize_type(att->atttypid, false, &tcategory, + &outfuncoid); datum_to_json(val, isnull, result, tcategory, outfuncoid, false); } @@ -699,7 +581,7 @@ add_json(Datum val, bool is_null, StringInfo result, outfuncoid = InvalidOid; } else - json_categorize_type(val_type, + json_categorize_type(val_type, false, &tcategory, &outfuncoid); datum_to_json(val, is_null, result, tcategory, outfuncoid, key_scalar); @@ -784,12 +666,13 @@ to_json_is_immutable(Oid typoid) JsonTypeCategory tcategory; Oid outfuncoid; - json_categorize_type(typoid, &tcategory, &outfuncoid); + json_categorize_type(typoid, false, &tcategory, &outfuncoid); switch (tcategory) { case JSONTYPE_BOOL: case JSONTYPE_JSON: + case JSONTYPE_JSONB: case JSONTYPE_NULL: return true; @@ -830,7 +713,7 @@ to_json(PG_FUNCTION_ARGS) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - json_categorize_type(val_type, + json_categorize_type(val_type, false, &tcategory, &outfuncoid); result = makeStringInfo(); @@ -880,7 +763,7 @@ json_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null) MemoryContextSwitchTo(oldcontext); appendStringInfoChar(state->str, '['); - json_categorize_type(arg_type, &state->val_category, + json_categorize_type(arg_type, false, &state->val_category, &state->val_output_func); } else @@ -1112,7 +995,7 @@ json_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine data type for argument %d", 1))); - json_categorize_type(arg_type, &state->key_category, + json_categorize_type(arg_type, false, &state->key_category, &state->key_output_func); arg_type = get_fn_expr_argtype(fcinfo->flinfo, 2); @@ -1122,7 +1005,7 @@ json_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine data type for argument %d", 2))); - json_categorize_type(arg_type, &state->val_category, + json_categorize_type(arg_type, false, &state->val_category, &state->val_output_func); appendStringInfoString(state->str, "{ "); diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index cf43c3f2de..fc64f56868 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -19,7 +19,6 @@ #include "funcapi.h" #include "libpq/pqformat.h" #include "miscadmin.h" -#include "parser/parse_coerce.h" #include "utils/builtins.h" #include "utils/date.h" #include "utils/datetime.h" @@ -37,29 +36,12 @@ typedef struct JsonbInState Node *escontext; } JsonbInState; -/* unlike with json categories, we need to treat json and jsonb differently */ -typedef enum /* type categories for datum_to_jsonb */ -{ - JSONBTYPE_NULL, /* null, so we didn't bother to identify */ - JSONBTYPE_BOOL, /* boolean (built-in types only) */ - JSONBTYPE_NUMERIC, /* numeric (ditto) */ - JSONBTYPE_DATE, /* we use special formatting for datetimes */ - JSONBTYPE_TIMESTAMP, /* we use special formatting for timestamp */ - JSONBTYPE_TIMESTAMPTZ, /* ... and timestamptz */ - JSONBTYPE_JSON, /* JSON */ - JSONBTYPE_JSONB, /* JSONB */ - JSONBTYPE_ARRAY, /* array */ - JSONBTYPE_COMPOSITE, /* composite */ - JSONBTYPE_JSONCAST, /* something with an explicit cast to JSON */ - JSONBTYPE_OTHER /* all else */ -} JsonbTypeCategory; - typedef struct JsonbAggState { JsonbInState *res; - JsonbTypeCategory key_category; + JsonTypeCategory key_category; Oid key_output_func; - JsonbTypeCategory val_category; + JsonTypeCategory val_category; Oid val_output_func; } JsonbAggState; @@ -72,19 +54,13 @@ static JsonParseErrorType jsonb_in_array_end(void *pstate); static JsonParseErrorType jsonb_in_object_field_start(void *pstate, char *fname, bool isnull); static void jsonb_put_escaped_value(StringInfo out, JsonbValue *scalarVal); static JsonParseErrorType jsonb_in_scalar(void *pstate, char *token, JsonTokenType tokentype); -static void jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid); static void composite_to_jsonb(Datum composite, JsonbInState *result); static void array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *dims, Datum *vals, bool *nulls, int *valcount, - JsonbTypeCategory tcategory, Oid outfuncoid); + JsonTypeCategory tcategory, Oid outfuncoid); static void array_to_jsonb_internal(Datum array, JsonbInState *result); -static void jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid); static void datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, - JsonbTypeCategory tcategory, Oid outfuncoid, + JsonTypeCategory tcategory, Oid outfuncoid, bool key_scalar); static void add_jsonb(Datum val, bool is_null, JsonbInState *result, Oid val_type, bool key_scalar); @@ -633,112 +609,6 @@ add_indent(StringInfo out, bool indent, int level) } -/* - * Determine how we want to render values of a given type in datum_to_jsonb. - * - * Given the datatype OID, return its JsonbTypeCategory, as well as the type's - * output function OID. If the returned category is JSONBTYPE_JSONCAST, - * we return the OID of the relevant cast function instead. - */ -static void -jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid) -{ - bool typisvarlena; - - /* Look through any domain */ - typoid = getBaseType(typoid); - - *outfuncoid = InvalidOid; - - /* - * We need to get the output function for everything except date and - * timestamp types, booleans, array and composite types, json and jsonb, - * and non-builtin types where there's a cast to json. In this last case - * we return the oid of the cast function instead. - */ - - switch (typoid) - { - case BOOLOID: - *tcategory = JSONBTYPE_BOOL; - break; - - case INT2OID: - case INT4OID: - case INT8OID: - case FLOAT4OID: - case FLOAT8OID: - case NUMERICOID: - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - *tcategory = JSONBTYPE_NUMERIC; - break; - - case DATEOID: - *tcategory = JSONBTYPE_DATE; - break; - - case TIMESTAMPOID: - *tcategory = JSONBTYPE_TIMESTAMP; - break; - - case TIMESTAMPTZOID: - *tcategory = JSONBTYPE_TIMESTAMPTZ; - break; - - case JSONBOID: - *tcategory = JSONBTYPE_JSONB; - break; - - case JSONOID: - *tcategory = JSONBTYPE_JSON; - break; - - default: - /* Check for arrays and composites */ - if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID - || typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID) - *tcategory = JSONBTYPE_ARRAY; - else if (type_is_rowtype(typoid)) /* includes RECORDOID */ - *tcategory = JSONBTYPE_COMPOSITE; - else - { - /* It's probably the general case ... */ - *tcategory = JSONBTYPE_OTHER; - - /* - * but first let's look for a cast to json (note: not to - * jsonb) if it's not built-in. - */ - if (typoid >= FirstNormalObjectId) - { - Oid castfunc; - CoercionPathType ctype; - - ctype = find_coercion_pathway(JSONOID, typoid, - COERCION_EXPLICIT, &castfunc); - if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc)) - { - *tcategory = JSONBTYPE_JSONCAST; - *outfuncoid = castfunc; - } - else - { - /* not a cast type, so just get the usual output func */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - } - else - { - /* any other builtin type */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - break; - } - } -} - /* * Turn a Datum into jsonb, adding it to the result JsonbInState. * @@ -753,7 +623,7 @@ jsonb_categorize_type(Oid typoid, */ static void datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, - JsonbTypeCategory tcategory, Oid outfuncoid, + JsonTypeCategory tcategory, Oid outfuncoid, bool key_scalar) { char *outputstr; @@ -770,11 +640,11 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, jb.type = jbvNull; } else if (key_scalar && - (tcategory == JSONBTYPE_ARRAY || - tcategory == JSONBTYPE_COMPOSITE || - tcategory == JSONBTYPE_JSON || - tcategory == JSONBTYPE_JSONB || - tcategory == JSONBTYPE_JSONCAST)) + (tcategory == JSONTYPE_ARRAY || + tcategory == JSONTYPE_COMPOSITE || + tcategory == JSONTYPE_JSON || + tcategory == JSONTYPE_JSONB || + tcategory == JSONTYPE_JSON)) { ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -782,18 +652,18 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, } else { - if (tcategory == JSONBTYPE_JSONCAST) + if (tcategory == JSONTYPE_CAST) val = OidFunctionCall1(outfuncoid, val); switch (tcategory) { - case JSONBTYPE_ARRAY: + case JSONTYPE_ARRAY: array_to_jsonb_internal(val, result); break; - case JSONBTYPE_COMPOSITE: + case JSONTYPE_COMPOSITE: composite_to_jsonb(val, result); break; - case JSONBTYPE_BOOL: + case JSONTYPE_BOOL: if (key_scalar) { outputstr = DatumGetBool(val) ? "true" : "false"; @@ -807,7 +677,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, jb.val.boolean = DatumGetBool(val); } break; - case JSONBTYPE_NUMERIC: + case JSONTYPE_NUMERIC: outputstr = OidOutputFunctionCall(outfuncoid, val); if (key_scalar) { @@ -845,26 +715,26 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, } } break; - case JSONBTYPE_DATE: + case JSONTYPE_DATE: jb.type = jbvString; jb.val.string.val = JsonEncodeDateTime(NULL, val, DATEOID, NULL); jb.val.string.len = strlen(jb.val.string.val); break; - case JSONBTYPE_TIMESTAMP: + case JSONTYPE_TIMESTAMP: jb.type = jbvString; jb.val.string.val = JsonEncodeDateTime(NULL, val, TIMESTAMPOID, NULL); jb.val.string.len = strlen(jb.val.string.val); break; - case JSONBTYPE_TIMESTAMPTZ: + case JSONTYPE_TIMESTAMPTZ: jb.type = jbvString; jb.val.string.val = JsonEncodeDateTime(NULL, val, TIMESTAMPTZOID, NULL); jb.val.string.len = strlen(jb.val.string.val); break; - case JSONBTYPE_JSONCAST: - case JSONBTYPE_JSON: + case JSONTYPE_CAST: + case JSONTYPE_JSON: { /* parse the json right into the existing result object */ JsonLexContext *lex; @@ -887,7 +757,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, pg_parse_json_or_ereport(lex, &sem); } break; - case JSONBTYPE_JSONB: + case JSONTYPE_JSONB: { Jsonb *jsonb = DatumGetJsonbP(val); JsonbIterator *it; @@ -931,7 +801,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, /* Now insert jb into result, unless we did it recursively */ if (!is_null && !scalar_jsonb && - tcategory >= JSONBTYPE_JSON && tcategory <= JSONBTYPE_JSONCAST) + tcategory >= JSONTYPE_JSON && tcategory <= JSONTYPE_CAST) { /* work has been done recursively */ return; @@ -976,7 +846,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, */ static void array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *dims, Datum *vals, - bool *nulls, int *valcount, JsonbTypeCategory tcategory, + bool *nulls, int *valcount, JsonTypeCategory tcategory, Oid outfuncoid) { int i; @@ -1020,7 +890,7 @@ array_to_jsonb_internal(Datum array, JsonbInState *result) int16 typlen; bool typbyval; char typalign; - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; ndim = ARR_NDIM(v); @@ -1037,8 +907,8 @@ array_to_jsonb_internal(Datum array, JsonbInState *result) get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign); - jsonb_categorize_type(element_type, - &tcategory, &outfuncoid); + json_categorize_type(element_type, true, + &tcategory, &outfuncoid); deconstruct_array(v, element_type, typlen, typbyval, typalign, &elements, &nulls, @@ -1084,7 +954,7 @@ composite_to_jsonb(Datum composite, JsonbInState *result) Datum val; bool isnull; char *attname; - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; JsonbValue v; Form_pg_attribute att = TupleDescAttr(tupdesc, i); @@ -1105,11 +975,12 @@ composite_to_jsonb(Datum composite, JsonbInState *result) if (isnull) { - tcategory = JSONBTYPE_NULL; + tcategory = JSONTYPE_NULL; outfuncoid = InvalidOid; } else - jsonb_categorize_type(att->atttypid, &tcategory, &outfuncoid); + json_categorize_type(att->atttypid, true, &tcategory, + &outfuncoid); datum_to_jsonb(val, isnull, result, tcategory, outfuncoid, false); } @@ -1122,7 +993,7 @@ composite_to_jsonb(Datum composite, JsonbInState *result) * Append JSON text for "val" to "result". * * This is just a thin wrapper around datum_to_jsonb. If the same type will be - * printed many times, avoid using this; better to do the jsonb_categorize_type + * printed many times, avoid using this; better to do the json_categorize_type * lookups only once. */ @@ -1130,7 +1001,7 @@ static void add_jsonb(Datum val, bool is_null, JsonbInState *result, Oid val_type, bool key_scalar) { - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; if (val_type == InvalidOid) @@ -1140,12 +1011,12 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, if (is_null) { - tcategory = JSONBTYPE_NULL; + tcategory = JSONTYPE_NULL; outfuncoid = InvalidOid; } else - jsonb_categorize_type(val_type, - &tcategory, &outfuncoid); + json_categorize_type(val_type, true, + &tcategory, &outfuncoid); datum_to_jsonb(val, is_null, result, tcategory, outfuncoid, key_scalar); } @@ -1160,33 +1031,33 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, bool to_jsonb_is_immutable(Oid typoid) { - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; - jsonb_categorize_type(typoid, &tcategory, &outfuncoid); + json_categorize_type(typoid, true, &tcategory, &outfuncoid); switch (tcategory) { - case JSONBTYPE_NULL: - case JSONBTYPE_BOOL: - case JSONBTYPE_JSON: - case JSONBTYPE_JSONB: + case JSONTYPE_NULL: + case JSONTYPE_BOOL: + case JSONTYPE_JSON: + case JSONTYPE_JSONB: return true; - case JSONBTYPE_DATE: - case JSONBTYPE_TIMESTAMP: - case JSONBTYPE_TIMESTAMPTZ: + case JSONTYPE_DATE: + case JSONTYPE_TIMESTAMP: + case JSONTYPE_TIMESTAMPTZ: return false; - case JSONBTYPE_ARRAY: + case JSONTYPE_ARRAY: return false; /* TODO recurse into elements */ - case JSONBTYPE_COMPOSITE: + case JSONTYPE_COMPOSITE: return false; /* TODO recurse into fields */ - case JSONBTYPE_NUMERIC: - case JSONBTYPE_JSONCAST: - case JSONBTYPE_OTHER: + case JSONTYPE_NUMERIC: + case JSONTYPE_CAST: + case JSONTYPE_OTHER: return func_volatile(outfuncoid) == PROVOLATILE_IMMUTABLE; } @@ -1202,7 +1073,7 @@ to_jsonb(PG_FUNCTION_ARGS) Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); JsonbInState result; - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; if (val_type == InvalidOid) @@ -1210,8 +1081,8 @@ to_jsonb(PG_FUNCTION_ARGS) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - jsonb_categorize_type(val_type, - &tcategory, &outfuncoid); + json_categorize_type(val_type, true, + &tcategory, &outfuncoid); memset(&result, 0, sizeof(JsonbInState)); @@ -1636,8 +1507,8 @@ jsonb_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null) WJB_BEGIN_ARRAY, NULL); MemoryContextSwitchTo(oldcontext); - jsonb_categorize_type(arg_type, &state->val_category, - &state->val_output_func); + json_categorize_type(arg_type, true, &state->val_category, + &state->val_output_func); } else { @@ -1816,8 +1687,8 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - jsonb_categorize_type(arg_type, &state->key_category, - &state->key_output_func); + json_categorize_type(arg_type, true, &state->key_category, + &state->key_output_func); arg_type = get_fn_expr_argtype(fcinfo->flinfo, 2); @@ -1826,8 +1697,8 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - jsonb_categorize_type(arg_type, &state->val_category, - &state->val_output_func); + json_categorize_type(arg_type, true, &state->val_category, + &state->val_output_func); } else { diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index 70cb922e6b..612bbf06a3 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -26,6 +26,7 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" #include "nodes/miscnodes.h" +#include "parser/parse_coerce.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -5685,3 +5686,113 @@ json_get_first_token(text *json, bool throw_error) return JSON_TOKEN_INVALID; /* invalid json */ } + +/* + * Determine how we want to print values of a given type in datum_to_json(b). + * + * Given the datatype OID, return its JsonTypeCategory, as well as the type's + * output function OID. If the returned category is JSONTYPE_CAST or + * JSOBTYPE_CASTJSON, we return the OID of the type->JSON cast function + * instead. + */ +void +json_categorize_type(Oid typoid, bool is_jsonb, + JsonTypeCategory *tcategory, Oid *outfuncoid) +{ + bool typisvarlena; + + /* Look through any domain */ + typoid = getBaseType(typoid); + + *outfuncoid = InvalidOid; + + /* + * We need to get the output function for everything except date and + * timestamp types, booleans, array and composite types, json and jsonb, + * and non-builtin types where there's a cast to json. In this last case + * we return the oid of the cast function instead. + */ + + switch (typoid) + { + case BOOLOID: + *tcategory = JSONTYPE_BOOL; + break; + + case INT2OID: + case INT4OID: + case INT8OID: + case FLOAT4OID: + case FLOAT8OID: + case NUMERICOID: + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + *tcategory = JSONTYPE_NUMERIC; + break; + + case DATEOID: + *tcategory = JSONTYPE_DATE; + break; + + case TIMESTAMPOID: + *tcategory = JSONTYPE_TIMESTAMP; + break; + + case TIMESTAMPTZOID: + *tcategory = JSONTYPE_TIMESTAMPTZ; + break; + + case JSONOID: + if (!is_jsonb) + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + *tcategory = JSONTYPE_JSON; + break; + + case JSONBOID: + if (!is_jsonb) + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + *tcategory = is_jsonb ? JSONTYPE_JSONB : JSONTYPE_JSON; + break; + + default: + /* Check for arrays and composites */ + if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID + || typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID) + *tcategory = JSONTYPE_ARRAY; + else if (type_is_rowtype(typoid)) /* includes RECORDOID */ + *tcategory = JSONTYPE_COMPOSITE; + else + { + /* + * It's probably the general case. But let's look for a cast + * to json (note: not to jsonb even if is_jsonb is true), if + * it's not built-in. + */ + *tcategory = JSONTYPE_OTHER; + if (typoid >= FirstNormalObjectId) + { + Oid castfunc; + CoercionPathType ctype; + + ctype = find_coercion_pathway(JSONOID, typoid, + COERCION_EXPLICIT, + &castfunc); + if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc)) + { + *outfuncoid = castfunc; + *tcategory = JSONTYPE_CAST; + } + else + { + /* non builtin type with no cast */ + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + } + } + else + { + /* any other builtin type */ + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + } + } + break; + } +} diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index a85203d4a4..27c2d20610 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -63,4 +63,24 @@ extern Jsonb *transform_jsonb_string_values(Jsonb *jsonb, void *action_state, extern text *transform_json_string_values(text *json, void *action_state, JsonTransformStringValuesAction transform_action); +/* Type categories for datum_to_json[b] and friends. */ +typedef enum +{ + JSONTYPE_NULL, /* null, so we didn't bother to identify */ + JSONTYPE_BOOL, /* boolean (built-in types only) */ + JSONTYPE_NUMERIC, /* numeric (ditto) */ + JSONTYPE_DATE, /* we use special formatting for datetimes */ + JSONTYPE_TIMESTAMP, + JSONTYPE_TIMESTAMPTZ, + JSONTYPE_JSON, /* JSON and JSONB */ + JSONTYPE_JSONB, /* JSONB (for datum_to_jsonb) */ + JSONTYPE_ARRAY, /* array */ + JSONTYPE_COMPOSITE, /* composite */ + JSONTYPE_CAST, /* something with an explicit cast to JSON */ + JSONTYPE_OTHER, /* all else */ +} JsonTypeCategory; + +void json_categorize_type(Oid typoid, bool is_jsonb, + JsonTypeCategory *tcategory, Oid *outfuncoid); + #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e941fb6c82..b10590a252 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1318,7 +1318,6 @@ JsonbIteratorToken JsonbPair JsonbParseState JsonbSubWorkspace -JsonbTypeCategory JsonbValue JumbleState JunkFilter -- 2.35.3
v5-0004-SQL-JSON-functions.patch (application/octet-stream, 53.5K) download From 4523dd06b1849f72eba0f326935c1a41ea2b6241 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:12:39 +0900 Subject: [PATCH v5 4/7] SQL JSON functions This Patch introduces three SQL standard JSON functions: JSON() JSON_SCALAR() JSON_SERIALIZE() JSON() produces json values from text, bytea, json or jsonb values, and has facilitites for handling duplicate keys. JSON_SCALAR() produces a json value from any scalar sql value, including json and jsonb. JSON_SERIALIZE() produces text or bytea from input which containis or represents json or jsonb; For the most part these functions don't add any significant new capabilities, but they will be of use to users wanting standard compliant JSON handling. Nikita Glukhov Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby. Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org --- doc/src/sgml/func.sgml | 71 +++++ src/backend/executor/execExpr.c | 30 ++ src/backend/executor/execExprInterp.c | 43 ++- src/backend/nodes/nodeFuncs.c | 30 ++ src/backend/parser/gram.y | 69 ++++- src/backend/parser/parse_expr.c | 234 +++++++++++++++- src/backend/parser/parse_target.c | 9 + src/backend/utils/adt/format_type.c | 4 + src/backend/utils/adt/json.c | 21 +- src/backend/utils/adt/jsonb.c | 48 +++- src/backend/utils/adt/ruleutils.c | 14 +- src/include/nodes/parsenodes.h | 48 ++++ src/include/nodes/primnodes.h | 5 +- src/include/parser/kwlist.h | 4 +- src/include/utils/jsonb.h | 2 +- src/include/utils/jsonfuncs.h | 4 + src/test/regress/expected/sqljson.out | 384 ++++++++++++++++++++++++++ src/test/regress/sql/sqljson.sql | 95 +++++++ src/tools/pgindent/typedefs.list | 1 + 19 files changed, 1067 insertions(+), 49 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 0b62e0c828..9cece06c18 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16001,6 +16001,77 @@ table2-mapping <returnvalue>{"a": "1", "b": "2"}</returnvalue> </para></entry> </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json constructor</primary></indexterm> + <function>json</function> ( + <replaceable>expression</replaceable> + <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional></optional> + <optional> { <literal>WITH</literal> | <literal>WITHOUT</literal> } <literal>UNIQUE</literal> <optional> <literal>KEYS</literal> </optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>json_data_type</replaceable> </optional>) + </para> + <para> + The <replaceable>expression</replaceable> can be any text type or a + <type>bytea</type> in UTF8 encoding. If the + <replaceable>expression</replaceable> is NULL, an + <acronym>SQL</acronym> null value is returned. + If <literal>WITH UNIQUE</literal> is specified, the + <replaceable>expression</replaceable> must not contain any duplicate + object keys. + </para> + <para> + <literal>json('{"a":123, "b":[true,"foo"], "a":"bar"}')</literal> + <returnvalue>{"a":123, "b":[true,"foo"], "a":"bar"}</returnvalue> + </para> + <para> + <literal>json('{"a":123,"b":[true,"foo"],"a":"bar"}' returning jsonb)</literal> + <returnvalue>{"a": "bar", "b": [true, "foo"]}</returnvalue> + </para> + </entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json_scalar</primary></indexterm> + <function>json_scalar</function> (<replaceable>expression</replaceable>) + <optional> <literal>RETURNING</literal> <replaceable>json_data_type</replaceable> </optional>) + </para> + <para> + Returns a JSON scalar value representing + <replaceable>expression</replaceable>. + If the input is NULL, an SQL NULL is returned. If the input is a number + or a boolean value, a corresponding JSON number or boolean value is + returned. For any other value a JSON string is returned. + </para> + <para> + <literal>json_scalar(123.45)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_scalar(CURRENT_TIMESTAMP)</literal> + <returnvalue>"2022-05-10T10:51:04.62128-04:00"</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <function>json_serialize</function> ( + <replaceable>expression</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional>) + </para> + <para> + Transforms an SQL/JSON value into a character or binary string. The + <replaceable>expression</replaceable> can be of any JSON type, any + character string type, or <type>bytea</type> in UTF8 encoding. + The returned type can be any character string type or + <type>bytea</type>. The default is <type>text</type>. + </para> + <para> + <literal>json_serialize('{ "a" : 1 } ' RETURNING bytea)</literal> + <returnvalue>\x7b20226122203a2031207d20</returnvalue> + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index bf3a08c5f0..6ca4098bef 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -48,6 +48,7 @@ #include "utils/array.h" #include "utils/builtins.h" #include "utils/datum.h" +#include "utils/jsonfuncs.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -2311,6 +2312,12 @@ ExecInitExprRec(Expr *node, ExprState *state, { ExecInitExprRec(ctor->func, state, resv, resnull); } + else if ((ctor->type == JSCTOR_JSON_PARSE && !ctor->unique) || + ctor->type == JSCTOR_JSON_SERIALIZE) + { + /* Use the value of the first argument as a result */ + ExecInitExprRec(linitial(args), state, resv, resnull); + } else { JsonConstructorExprState *jcstate; @@ -2349,6 +2356,29 @@ ExecInitExprRec(Expr *node, ExprState *state, argno++; } + /* prepare type cache for datum_to_json[b]() */ + if (ctor->type == JSCTOR_JSON_SCALAR) + { + bool is_jsonb = + ctor->returning->format->format_type == JS_FORMAT_JSONB; + + jcstate->arg_type_cache = + palloc(sizeof(*jcstate->arg_type_cache) * nargs); + + for (int i = 0; i < nargs; i++) + { + JsonTypeCategory category; + Oid outfuncid; + Oid typid = jcstate->arg_types[i]; + + json_categorize_type(typid, is_jsonb, + &category, &outfuncid); + + jcstate->arg_type_cache[i].outfuncid = outfuncid; + jcstate->arg_type_cache[i].category = (int) category; + } + } + ExprEvalPushStep(state, &scratch); } diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 851946a927..76e59691e5 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -3992,7 +3992,7 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_values, jcstate->arg_nulls, jcstate->arg_types, - jcstate->constructor->absent_on_null); + ctor->absent_on_null); else if (ctor->type == JSCTOR_JSON_OBJECT) res = (is_jsonb ? jsonb_build_object_worker : @@ -4002,6 +4002,47 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_types, jcstate->constructor->absent_on_null, jcstate->constructor->unique); + else if (ctor->type == JSCTOR_JSON_SCALAR) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + Oid outfuncid = jcstate->arg_type_cache[0].outfuncid; + JsonTypeCategory category = (JsonTypeCategory) + jcstate->arg_type_cache[0].category; + + if (is_jsonb) + res = to_jsonb_worker(value, category, outfuncid); + else + res = to_json_worker(value, category, outfuncid); + } + } + else if (ctor->type == JSCTOR_JSON_PARSE) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + text *js = DatumGetTextP(value); + + if (is_jsonb) + res = jsonb_from_text(js, true); + else + { + (void) json_validate(js, true, true); + res = value; + } + } + } else elog(ERROR, "invalid JsonConstructorExpr type %d", ctor->type); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index c41e6bb984..dda964bd19 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -3901,6 +3901,36 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonParseExpr: + { + JsonParseExpr *jpe = (JsonParseExpr *) node; + + if (WALK(jpe->expr)) + return true; + if (WALK(jpe->output)) + return true; + } + break; + case T_JsonScalarExpr: + { + JsonScalarExpr *jse = (JsonScalarExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; + case T_JsonSerializeExpr: + { + JsonSerializeExpr *jse = (JsonSerializeExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; case T_JsonConstructorExpr: { JsonConstructorExpr *ctor = (JsonConstructorExpr *) node; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index edb6c00ece..341b002dc7 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -566,7 +566,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <list> copy_options %type <typnam> Typename SimpleTypename ConstTypename - GenericType Numeric opt_float + GenericType Numeric opt_float JsonType Character ConstCharacter CharacterWithLength CharacterWithoutLength ConstDatetime ConstInterval @@ -647,7 +647,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> json_format_clause_opt json_value_expr - json_output_clause_opt + json_returning_clause_opt json_name_and_value json_aggregate_func %type <list> json_name_and_value_list @@ -659,7 +659,6 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt - /* * Non-keyword token types. These are hard-wired into the "flex" lexer. * They must be listed first so that their numeric codes do not depend on @@ -723,6 +722,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG + JSON_SCALAR JSON_SERIALIZE KEY KEYS @@ -13981,6 +13981,7 @@ SimpleTypename: $$->typmods = list_make2(makeIntConst(INTERVAL_FULL_RANGE, -1), makeIntConst($3, @3)); } + | JsonType { $$ = $1; } ; /* We have a separate ConstTypename to allow defaulting fixed-length @@ -13999,6 +14000,7 @@ ConstTypename: | ConstBit { $$ = $1; } | ConstCharacter { $$ = $1; } | ConstDatetime { $$ = $1; } + | JsonType { $$ = $1; } ; /* @@ -14367,6 +14369,13 @@ interval_second: } ; +JsonType: + JSON + { + $$ = SystemTypeName("json"); + $$->location = @1; + } + ; /***************************************************************************** * @@ -15561,7 +15570,7 @@ func_expr_common_subexpr: | JSON_OBJECT '(' json_name_and_value_list json_object_constructor_null_clause_opt json_key_uniqueness_constraint_opt - json_output_clause_opt ')' + json_returning_clause_opt ')' { JsonObjectConstructor *n = makeNode(JsonObjectConstructor); @@ -15572,7 +15581,7 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } - | JSON_OBJECT '(' json_output_clause_opt ')' + | JSON_OBJECT '(' json_returning_clause_opt ')' { JsonObjectConstructor *n = makeNode(JsonObjectConstructor); @@ -15586,7 +15595,7 @@ func_expr_common_subexpr: | JSON_ARRAY '(' json_value_expr_list json_array_constructor_null_clause_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayConstructor *n = makeNode(JsonArrayConstructor); @@ -15601,7 +15610,7 @@ func_expr_common_subexpr: select_no_parens json_format_clause_opt /* json_array_constructor_null_clause_opt */ - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayQueryConstructor *n = makeNode(JsonArrayQueryConstructor); @@ -15614,7 +15623,7 @@ func_expr_common_subexpr: $$ = (Node *) n; } | JSON_ARRAY '(' - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayConstructor *n = makeNode(JsonArrayConstructor); @@ -15625,7 +15634,37 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } - ; + | JSON '(' json_value_expr json_key_uniqueness_constraint_opt + json_returning_clause_opt ')' + { + JsonParseExpr *n = makeNode(JsonParseExpr); + + n->expr = (JsonValueExpr *) $3; + n->unique_keys = $4; + n->output = (JsonOutput *) $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SCALAR '(' a_expr json_returning_clause_opt ')' + { + JsonScalarExpr *n = makeNode(JsonScalarExpr); + + n->expr = (Expr *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SERIALIZE '(' json_value_expr json_returning_clause_opt ')' + { + JsonSerializeExpr *n = makeNode(JsonSerializeExpr); + + n->expr = (JsonValueExpr *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + ; + /* * SQL/XML support @@ -16373,7 +16412,7 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; -json_output_clause_opt: +json_returning_clause_opt: RETURNING Typename json_format_clause_opt { JsonOutput *n = makeNode(JsonOutput); @@ -16446,7 +16485,7 @@ json_aggregate_func: json_name_and_value json_object_constructor_null_clause_opt json_key_uniqueness_constraint_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonObjectAgg *n = makeNode(JsonObjectAgg); @@ -16464,7 +16503,7 @@ json_aggregate_func: json_value_expr json_array_aggregate_order_by_clause_opt json_array_constructor_null_clause_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayAgg *n = makeNode(JsonArrayAgg); @@ -17064,7 +17103,6 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION - | JSON | KEY | KEYS | LABEL @@ -17279,10 +17317,13 @@ col_name_keyword: | INT_P | INTEGER | INTERVAL + | JSON | JSON_ARRAY | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | LEAST | NATIONAL | NCHAR @@ -17643,6 +17684,8 @@ bare_label_keyword: | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | KEY | KEYS | LABEL diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 8d7a44408c..198975cb8c 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -86,6 +86,10 @@ static Node *transformJsonArrayQueryConstructor(ParseState *pstate, static Node *transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg); static Node *transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg); static Node *transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred); +static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); +static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); +static Node *transformJsonSerializeExpr(ParseState *pstate, + JsonSerializeExpr * expr); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -337,6 +341,18 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonIsPredicate(pstate, (JsonIsPredicate *) expr); break; + case T_JsonParseExpr: + result = transformJsonParseExpr(pstate, (JsonParseExpr *) expr); + break; + + case T_JsonScalarExpr: + result = transformJsonScalarExpr(pstate, (JsonScalarExpr *) expr); + break; + + case T_JsonSerializeExpr: + result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3208,7 +3224,8 @@ makeJsonByteaToTextConversion(Node *expr, JsonFormat *format, int location) */ static Node * transformJsonValueExpr(ParseState *pstate, char *constructName, - JsonValueExpr *ve, JsonFormatType default_format) + JsonValueExpr *ve, JsonFormatType default_format, + Oid targettype) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3250,12 +3267,14 @@ transformJsonValueExpr(ParseState *pstate, char *constructName, else format = default_format; - if (format != JS_FORMAT_DEFAULT) + if (format != JS_FORMAT_DEFAULT || + (OidIsValid(targettype) && exprtype != targettype)) { - Oid targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; Node *coerced; + bool cast_is_needed = OidIsValid(targettype); - if (exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) + if (!cast_is_needed && + exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), errmsg(ve->format->format_type == JS_FORMAT_DEFAULT ? @@ -3271,6 +3290,9 @@ transformJsonValueExpr(ParseState *pstate, char *constructName, exprtype = TEXTOID; } + if (!OidIsValid(targettype)) + targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; + /* Try to coerce to the target type. */ coerced = coerce_to_target_type(pstate, expr, exprtype, targettype, -1, @@ -3281,11 +3303,20 @@ transformJsonValueExpr(ParseState *pstate, char *constructName, if (!coerced) { /* If coercion failed, use to_json()/to_jsonb() functions. */ - Oid fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; - FuncExpr *fexpr = makeFuncExpr(fnoid, targettype, - list_make1(expr), - InvalidOid, InvalidOid, - COERCE_EXPLICIT_CALL); + FuncExpr *fexpr; + Oid fnoid; + + if (cast_is_needed) /* only CAST is allowed */ + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(exprtype), + format_type_be(targettype)), + parser_errposition(pstate, location))); + + fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; + fexpr = makeFuncExpr(fnoid, targettype, list_make1(expr), + InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL); fexpr->location = location; @@ -3582,7 +3613,8 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) Node *key = transformExprRecurse(pstate, (Node *) kv->key); Node *val = transformJsonValueExpr(pstate, "JSON_OBJECT()", kv->value, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, key); args = lappend(args, val); @@ -3766,9 +3798,8 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) Oid aggtype; key = transformExprRecurse(pstate, (Node *) agg->arg->key); - val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", - agg->arg->value, - JS_FORMAT_DEFAULT); + val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", agg->arg->value, + JS_FORMAT_DEFAULT, InvalidOid); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3826,7 +3857,7 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", agg->arg, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, InvalidOid); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3874,7 +3905,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, "JSON_ARRAY()", jsval, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, val); } @@ -3957,3 +3989,175 @@ transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred) return makeJsonIsPredicate(expr, NULL, pred->item_type, pred->unique_keys, pred->location); } + +/* + * Transform the output clause of a JSON_*() expression if there is one and + * create one if not. + */ +static JsonReturning * +transformJsonReturning(ParseState *pstate, JsonOutput *output, const char *fname) +{ + JsonReturning *returning; + + if (output) + { + returning = transformJsonOutput(pstate, output, false); + + Assert(OidIsValid(returning->typid)); + + if (returning->typid != JSONOID && returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), fname), + parser_errposition(pstate, output->typeName->location))); + } + else + { + /* Output type is JSON by default. */ + Oid targettype = JSONOID; + JsonFormatType format = JS_FORMAT_JSON; + + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(format, JS_ENC_DEFAULT, -1); + returning->typid = targettype; + returning->typmod = -1; + } + + return returning; +} + +/* + * Transform a JSON() expression. + */ +static Node * +transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) +{ + JsonOutput *output = jsexpr->output; + JsonReturning *returning; + Node *arg; + + /* Disallow FORMAT specification in the RETURNING clause. */ + if (output) + { + JsonFormat *format = output->returning->format; + + if(format->format_type != JS_FORMAT_DEFAULT || + format->encoding != JS_ENC_DEFAULT) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot specify FORMAT in RETURNING clause of JSON()"), + parser_errposition(pstate, format->location))); + } + + returning = transformJsonReturning(pstate, output, "JSON()"); + + if (jsexpr->unique_keys) + { + /* + * Coerce string argument to text and then to json[b] in the executor + * node with key uniqueness check. + */ + JsonValueExpr *jve = jsexpr->expr; + Oid arg_type; + + arg = transformJsonParseArg(pstate, (Node *) jve->raw_expr, jve->format, + &arg_type); + + if (arg_type != TEXTOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use non-string types with WITH UNIQUE KEYS clause"), + parser_errposition(pstate, jsexpr->location))); + } + else + { + /* + * Coerce argument to target type using CAST for compatibility with PG + * function-like CASTs. + */ + arg = transformJsonValueExpr(pstate, "JSON()", jsexpr->expr, + JS_FORMAT_JSON, returning->typid); + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, + returning, jsexpr->unique_keys, false, + jsexpr->location); +} + +/* + * Transform a JSON_SCALAR() expression. + */ +static Node * +transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *jsexpr) +{ + Node *arg = transformExprRecurse(pstate, (Node *) jsexpr->expr); + JsonOutput *output = jsexpr->output; + JsonReturning *returning; + + /* Disallow FORMAT specification in the RETURNING clause. */ + if (output) + { + JsonFormat *format = output->returning->format; + + if(format->format_type != JS_FORMAT_DEFAULT || + format->encoding != JS_ENC_DEFAULT) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot specify FORMAT in RETURNING clause of JSON_SCALAR()"), + parser_errposition(pstate, format->location))); + } + + returning = transformJsonReturning(pstate, output, "JSON_SCALAR()"); + + if (exprType(arg) == UNKNOWNOID) + arg = coerce_to_specific_type(pstate, arg, TEXTOID, "JSON_SCALAR"); + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SCALAR, list_make1(arg), NULL, + returning, false, false, jsexpr->location); +} + +/* + * Transform a JSON_SERIALIZE() expression. + */ +static Node * +transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) +{ + Node *arg = transformJsonValueExpr(pstate, "JSON_SERIALIZE()", + expr->expr, + JS_FORMAT_JSON, + InvalidOid); + JsonReturning *returning; + + if (expr->output) + { + returning = transformJsonOutput(pstate, expr->output, true); + + if (returning->typid != BYTEAOID) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(returning->typid, &typcategory, + &typispreferred); + if (typcategory != TYPCATEGORY_STRING) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), + "JSON_SERIALIZE()"), + errhint("Try returning a string type or bytea."))); + } + } + else + { + /* RETURNING TEXT FORMAT JSON is by default */ + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(JS_FORMAT_JSON, JS_ENC_DEFAULT, -1); + returning->typid = TEXTOID; + returning->typmod = -1; + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), + NULL, returning, false, false, expr->location); +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4cca97ff9c..520d4f2a23 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1953,6 +1953,15 @@ FigureColnameInternal(Node *node, char **name) /* make XMLSERIALIZE act like a regular function */ *name = "xmlserialize"; return 2; + case T_JsonParseExpr: + *name = "json"; + return 2; + case T_JsonScalarExpr: + *name = "json_scalar"; + return 2; + case T_JsonSerializeExpr: + *name = "json_serialize"; + return 2; case T_JsonObjectConstructor: /* make JSON_OBJECT act like a regular function */ *name = "json_object"; diff --git a/src/backend/utils/adt/format_type.c b/src/backend/utils/adt/format_type.c index 12402a0637..36c45a3978 100644 --- a/src/backend/utils/adt/format_type.c +++ b/src/backend/utils/adt/format_type.c @@ -294,6 +294,10 @@ format_type_extended(Oid type_oid, int32 typemod, bits16 flags) else buf = pstrdup("character varying"); break; + + case JSONOID: + buf = pstrdup("json"); + break; } if (buf == NULL) diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index f6bef9c148..c316f848e1 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -653,6 +653,20 @@ row_to_json_pretty(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); } +/* + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. + */ +Datum +to_json_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid) +{ + StringInfo result = makeStringInfo(); + + datum_to_json(val, false, result, tcategory, outfuncoid, false); + + return PointerGetDatum(cstring_to_text_with_len(result->data, result->len)); +} + /* * Is the given type immutable when coming out of a JSON context? * @@ -704,7 +718,6 @@ to_json(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - StringInfo result; JsonTypeCategory tcategory; Oid outfuncoid; @@ -716,11 +729,7 @@ to_json(PG_FUNCTION_ARGS) json_categorize_type(val_type, false, &tcategory, &outfuncoid); - result = makeStringInfo(); - - datum_to_json(val, false, result, tcategory, outfuncoid, false); - - PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); + PG_RETURN_DATUM(to_json_worker(val, tcategory, outfuncoid)); } /* diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index fc64f56868..06ba409e64 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -33,6 +33,7 @@ typedef struct JsonbInState { JsonbParseState *parseState; JsonbValue *res; + bool unique_keys; Node *escontext; } JsonbInState; @@ -45,7 +46,8 @@ typedef struct JsonbAggState Oid val_output_func; } JsonbAggState; -static inline Datum jsonb_from_cstring(char *json, int len, Node *escontext); +static inline Datum jsonb_from_cstring(char *json, int len, bool unique_keys, + Node *escontext); static bool checkStringLen(size_t len, Node *escontext); static JsonParseErrorType jsonb_in_object_start(void *pstate); static JsonParseErrorType jsonb_in_object_end(void *pstate); @@ -76,7 +78,7 @@ jsonb_in(PG_FUNCTION_ARGS) { char *json = PG_GETARG_CSTRING(0); - return jsonb_from_cstring(json, strlen(json), fcinfo->context); + return jsonb_from_cstring(json, strlen(json), false, fcinfo->context); } /* @@ -100,7 +102,7 @@ jsonb_recv(PG_FUNCTION_ARGS) else elog(ERROR, "unsupported jsonb version number %d", version); - return jsonb_from_cstring(str, nbytes, NULL); + return jsonb_from_cstring(str, nbytes, false, NULL); } /* @@ -141,6 +143,18 @@ jsonb_send(PG_FUNCTION_ARGS) PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); } +/* + * Note: exported for use in SQL/JSON executor functions. + */ +Datum +jsonb_from_text(text *js, bool unique_keys) +{ + return jsonb_from_cstring(VARDATA_ANY(js), + VARSIZE_ANY_EXHDR(js), + unique_keys, + NULL); +} + /* * Get the type name of a jsonb container. */ @@ -234,7 +248,7 @@ jsonb_typeof(PG_FUNCTION_ARGS) * instead of being thrown. */ static inline Datum -jsonb_from_cstring(char *json, int len, Node *escontext) +jsonb_from_cstring(char *json, int len, bool unique_keys, Node *escontext) { JsonLexContext *lex; JsonbInState state; @@ -244,6 +258,7 @@ jsonb_from_cstring(char *json, int len, Node *escontext) memset(&sem, 0, sizeof(sem)); lex = makeJsonLexContextCstringLen(json, len, GetDatabaseEncoding(), true); + state.unique_keys = unique_keys; state.escontext = escontext; sem.semstate = (void *) &state; @@ -280,6 +295,7 @@ jsonb_in_object_start(void *pstate) JsonbInState *_state = (JsonbInState *) pstate; _state->res = pushJsonbValue(&_state->parseState, WJB_BEGIN_OBJECT, NULL); + _state->parseState->unique_keys = _state->unique_keys; return JSON_SUCCESS; } @@ -1021,6 +1037,23 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, datum_to_jsonb(val, is_null, result, tcategory, outfuncoid, key_scalar); } + +/* + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. + */ +Datum +to_jsonb_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid) +{ + JsonbInState result; + + memset(&result, 0, sizeof(JsonbInState)); + + datum_to_jsonb(val, false, &result, tcategory, outfuncoid, false); + + return JsonbPGetDatum(JsonbValueToJsonb(result.res)); +} + /* * Is the given type immutable when coming out of a JSONB context? * @@ -1072,7 +1105,6 @@ to_jsonb(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - JsonbInState result; JsonTypeCategory tcategory; Oid outfuncoid; @@ -1084,11 +1116,7 @@ to_jsonb(PG_FUNCTION_ARGS) json_categorize_type(val_type, true, &tcategory, &outfuncoid); - memset(&result, 0, sizeof(JsonbInState)); - - datum_to_jsonb(val, false, &result, tcategory, outfuncoid, false); - - PG_RETURN_POINTER(JsonbValueToJsonb(result.res)); + PG_RETURN_DATUM(to_jsonb_worker(val, tcategory, outfuncoid)); } Datum diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d3a973d86b..d1b03d6cb2 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -10832,6 +10832,15 @@ get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, case JSCTOR_JSON_ARRAY: funcname = "JSON_ARRAY"; break; + case JSCTOR_JSON_PARSE: + funcname = "JSON"; + break; + case JSCTOR_JSON_SCALAR: + funcname = "JSON_SCALAR"; + break; + case JSCTOR_JSON_SERIALIZE: + funcname = "JSON_SERIALIZE"; + break; default: elog(ERROR, "invalid JsonConstructorType %d", ctor->type); } @@ -10879,7 +10888,10 @@ get_json_constructor_options(JsonConstructorExpr *ctor, StringInfo buf) if (ctor->unique) appendStringInfoString(buf, " WITH UNIQUE KEYS"); - get_json_returning(ctor->returning, buf, true); + if (!((ctor->type == JSCTOR_JSON_PARSE || + ctor->type == JSCTOR_JSON_SCALAR) && + ctor->returning->typid == JSONOID)) + get_json_returning(ctor->returning, buf, true); } /* diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index efb5c3e098..d926713bd9 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1716,6 +1716,17 @@ typedef struct TriggerTransition /* Nodes for SQL/JSON support */ +/* + * JsonQuotes - + * representation of [KEEP|OMIT] QUOTES clause for JSON_QUERY() + */ +typedef enum JsonQuotes +{ + JS_QUOTES_UNSPEC, /* unspecified */ + JS_QUOTES_KEEP, /* KEEP QUOTES */ + JS_QUOTES_OMIT /* OMIT QUOTES */ +} JsonQuotes; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1739,6 +1750,43 @@ typedef struct JsonKeyValue JsonValueExpr *value; /* JSON value expression */ } JsonKeyValue; +/* + * JsonParseExpr - + * untransformed representation of JSON() + */ +typedef struct JsonParseExpr +{ + NodeTag type; + JsonValueExpr *expr; /* string expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + bool unique_keys; /* WITH UNIQUE KEYS? */ + int location; /* token location, or -1 if unknown */ +} JsonParseExpr; + +/* + * JsonScalarExpr - + * untransformed representation of JSON_SCALAR() + */ +typedef struct JsonScalarExpr +{ + NodeTag type; + Expr *expr; /* scalar expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonScalarExpr; + +/* + * JsonSerializeExpr - + * untransformed representation of JSON_SERIALIZE() function + */ +typedef struct JsonSerializeExpr +{ + NodeTag type; + JsonValueExpr *expr; /* json value expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonSerializeExpr; + /* * JsonObjectConstructor - * untransformed representation of JSON_OBJECT() constructor diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 0d2df069b3..85e484bf43 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1612,7 +1612,10 @@ typedef enum JsonConstructorType JSCTOR_JSON_OBJECT = 1, JSCTOR_JSON_ARRAY = 2, JSCTOR_JSON_OBJECTAGG = 3, - JSCTOR_JSON_ARRAYAGG = 4 + JSCTOR_JSON_ARRAYAGG = 4, + JSCTOR_JSON_SCALAR = 5, + JSCTOR_JSON_SERIALIZE = 6, + JSCTOR_JSON_PARSE = 7 } JsonConstructorType; /* diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index f5b2e61ca5..5984dcfa4b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -230,11 +230,13 @@ PG_KEYWORD("is", IS, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("isnull", ISNULL, TYPE_FUNC_NAME_KEYWORD, AS_LABEL) PG_KEYWORD("isolation", ISOLATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) -PG_KEYWORD("json", JSON, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index 649a1644f2..f928e6142a 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -368,7 +368,6 @@ typedef struct JsonbIterator struct JsonbIterator *parent; } JsonbIterator; - /* Convenience macros */ static inline Jsonb * DatumGetJsonbP(Datum d) @@ -418,6 +417,7 @@ extern void JsonbHashScalarValueExtended(const JsonbValue *scalarVal, uint64 *hash, uint64 seed); /* jsonb.c support functions */ +extern Datum jsonb_from_text(text *js, bool unique_keys); extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index 27c2d20610..586d948c94 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -82,5 +82,9 @@ typedef enum void json_categorize_type(Oid typoid, bool is_jsonb, JsonTypeCategory *tcategory, Oid *outfuncoid); +extern Datum to_json_worker(Datum val, JsonTypeCategory tcategory, + Oid outfuncoid); +extern Datum to_jsonb_worker(Datum val, JsonTypeCategory tcategory, + Oid outfuncoid); #endif diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index d73c7e2c6c..ddea8a072f 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -1,3 +1,345 @@ +-- JSON() +SELECT JSON(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON(); + ^ +SELECT JSON(NULL); + json +------ + +(1 row) + +SELECT JSON('{ "a" : 1 } '); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +ERROR: JSON ENCODING clause is only allowed for bytea input type +LINE 1: SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); + ^ +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + pg_typeof +----------- + json +(1 row) + +SELECT JSON(' 1 '::json); + json +--------- + 1 +(1 row) + +SELECT JSON(' 1 '::jsonb); + json +------ + 1 +(1 row) + +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +ERROR: cannot use non-string types with WITH UNIQUE KEYS clause +LINE 1: SELECT JSON(' 1 '::json WITH UNIQUE KEYS); + ^ +SELECT JSON(123); +ERROR: cannot cast type integer to json +LINE 1: SELECT JSON(123); + ^ +SELECT JSON('{"a": 1, "a": 2}'); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +ERROR: duplicate JSON object key value +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); + QUERY PLAN +----------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); + QUERY PLAN +------------------------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON ENCODING UTF8) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); + QUERY PLAN +---------------------------------------------- + Result + Output: JSON('123'::text WITH UNIQUE KEYS) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +SELECT JSON('123' RETURNING text); +ERROR: cannot use RETURNING type text in JSON() +LINE 1: SELECT JSON('123' RETURNING text); + ^ +SELECT JSON('123' RETURNING text FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON() +LINE 1: SELECT JSON('123' RETURNING text FORMAT JSON); + ^ +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING json); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING jsonb); + QUERY PLAN +---------------------------------------------- + Result + Output: JSON('123'::jsonb RETURNING jsonb) +(2 rows) + +SELECT pg_typeof(JSON('123')); + pg_typeof +----------- + json +(1 row) + +SELECT pg_typeof(JSON('123' RETURNING json)); + pg_typeof +----------- + json +(1 row) + +SELECT pg_typeof(JSON('123' RETURNING jsonb)); + pg_typeof +----------- + jsonb +(1 row) + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SCALAR(); + ^ +SELECT JSON_SCALAR(NULL); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(NULL::int); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(123); + json_scalar +------------- + 123 +(1 row) + +SELECT JSON_SCALAR(123.45); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(123.45::numeric); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(true); + json_scalar +------------- + true +(1 row) + +SELECT JSON_SCALAR(false); + json_scalar +------------- + false +(1 row) + +SELECT JSON_SCALAR(' 123.45'); + json_scalar +------------- + " 123.45" +(1 row) + +SELECT JSON_SCALAR('2020-06-07'::date); + json_scalar +-------------- + "2020-06-07" +(1 row) + +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); + json_scalar +----------------------- + "2020-06-07T01:02:03" +(1 row) + +SELECT JSON_SCALAR('{}'::json); + json_scalar +------------- + {} +(1 row) + +SELECT JSON_SCALAR('{}'::jsonb); + json_scalar +------------- + {} +(1 row) + +SELECT JSON_SCALAR(123 RETURNING jsonb FORMAT JSON ENCODING UTF8); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_SCALAR() +LINE 1: SELECT JSON_SCALAR(123 RETURNING jsonb FORMAT JSON ENCODING ... + ^ +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); + QUERY PLAN +---------------------------- + Result + Output: JSON_SCALAR(123) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); + QUERY PLAN +------------------------------------ + Result + Output: JSON_SCALAR('123'::text) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING json); + QUERY PLAN +---------------------------- + Result + Output: JSON_SCALAR(123) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING jsonb); + QUERY PLAN +-------------------------------------------- + Result + Output: JSON_SCALAR(123 RETURNING jsonb) +(2 rows) + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SERIALIZE(); + ^ +SELECT JSON_SERIALIZE(NULL); + json_serialize +---------------- + +(1 row) + +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } '); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('1'); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('1' FORMAT JSON); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); + json_serialize +---------------------------- + \x7b20226122203a2031207d20 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + pg_typeof +----------- + text +(1 row) + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); +ERROR: cannot use RETURNING type jsonb in JSON_SERIALIZE() +HINT: Try returning a string type or bytea. +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); + QUERY PLAN +----------------------------------------------------- + Result + Output: JSON_SERIALIZE('{}'::json RETURNING text) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + QUERY PLAN +------------------------------------------------------ + Result + Output: JSON_SERIALIZE('{}'::json RETURNING bytea) +(2 rows) + -- JSON_OBJECT() SELECT JSON_OBJECT(); json_object @@ -630,6 +972,13 @@ ERROR: duplicate JSON object key SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); ERROR: duplicate JSON object key +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + json_objectagg +------------------ + {"1": 1, "2": 2} +(1 row) + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -645,6 +994,41 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); CREATE OR REPLACE VIEW public.json_object_view AS SELECT JSON_OBJECT('foo' : '1'::text FORMAT JSON, 'bar' : 'baz'::text RETURNING json) AS "json_object" DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + a | json_objectagg +---------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(2 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":1,"v":null} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1, "2" : 2 } + {"k":1,"v":null} | { "1" : 1, "2" : 2 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 4fd820fd51..f9afae0d42 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -1,3 +1,77 @@ +-- JSON() +SELECT JSON(); +SELECT JSON(NULL); +SELECT JSON('{ "a" : 1 } '); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + +SELECT JSON(' 1 '::json); +SELECT JSON(' 1 '::jsonb); +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +SELECT JSON(123); + +SELECT JSON('{"a": 1, "a": 2}'); +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + +SELECT JSON('123' RETURNING text); +SELECT JSON('123' RETURNING text FORMAT JSON); -- RETURNING FORMAT not allowed + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING json); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING jsonb); +SELECT pg_typeof(JSON('123')); +SELECT pg_typeof(JSON('123' RETURNING json)); +SELECT pg_typeof(JSON('123' RETURNING jsonb)); + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +SELECT JSON_SCALAR(NULL); +SELECT JSON_SCALAR(NULL::int); +SELECT JSON_SCALAR(123); +SELECT JSON_SCALAR(123.45); +SELECT JSON_SCALAR(123.45::numeric); +SELECT JSON_SCALAR(true); +SELECT JSON_SCALAR(false); +SELECT JSON_SCALAR(' 123.45'); +SELECT JSON_SCALAR('2020-06-07'::date); +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); +SELECT JSON_SCALAR('{}'::json); +SELECT JSON_SCALAR('{}'::jsonb); +SELECT JSON_SCALAR(123 RETURNING jsonb FORMAT JSON ENCODING UTF8); -- RETURNING FORMAT not allowed + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING json); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING jsonb); + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +SELECT JSON_SERIALIZE(NULL); +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); +SELECT JSON_SERIALIZE('{ "a" : 1 } '); +SELECT JSON_SERIALIZE('1'); +SELECT JSON_SERIALIZE('1' FORMAT JSON); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); + + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + -- JSON_OBJECT() SELECT JSON_OBJECT(); SELECT JSON_OBJECT(RETURNING json); @@ -216,6 +290,9 @@ FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -227,6 +304,24 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index b10590a252..d251fb9a91 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1296,6 +1296,7 @@ JsonPathPredicateCallback JsonPathString JsonReturning JsonSemAction +JsonSerializeExpr JsonTokenType JsonTransformStringValuesAction JsonTypeCategory -- 2.35.3
v5-0006-JSON_TABLE.patch (application/octet-stream, 160.4K) download From ea5a61c950424c1c5d01024d67ac674f80350ffc Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:14:12 +0900 Subject: [PATCH v5 6/7] JSON_TABLE This feature allows jsonb data to be treated as a table and thus used in a FROM clause like other tabular data. Data can be selected from the jsonb using jsonpath expressions, and hoisted out of nested structures in the jsonb to form multiple rows, more or less like an outer join. This also adds the PLAN clauses for JSON_TABLE, which allow the user to specify how data from nested paths are joined, allowing considerable freedom in shaping the tabular output of JSON_TABLE. PLAN DEFAULT allows the user to specify the global strategies when dealing with sibling or child nested paths. The is often sufficient to achieve the necessary goal, and is considerably simpler than the full PLAN clause, which allows the user to specify the strategy to be used for each named nested path. Nikita Glukhov Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zhihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby. Discussion: https://postgr.es/m/7e2cb85d-24cf-4abb-30a5-1a33715959bd@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org --- doc/src/sgml/func.sgml | 448 ++++++++ src/backend/commands/explain.c | 8 +- src/backend/executor/execExprInterp.c | 5 + src/backend/executor/nodeTableFuncscan.c | 27 +- src/backend/nodes/makefuncs.c | 19 + src/backend/nodes/nodeFuncs.c | 30 + src/backend/parser/Makefile | 1 + src/backend/parser/gram.y | 476 ++++++++- src/backend/parser/meson.build | 1 + src/backend/parser/parse_clause.c | 14 +- src/backend/parser/parse_expr.c | 35 +- src/backend/parser/parse_jsontable.c | 739 +++++++++++++ src/backend/parser/parse_relation.c | 5 +- src/backend/parser/parse_target.c | 3 + src/backend/utils/adt/jsonpath_exec.c | 543 ++++++++++ src/backend/utils/adt/ruleutils.c | 279 ++++- src/include/nodes/execnodes.h | 2 + src/include/nodes/makefuncs.h | 2 + src/include/nodes/parsenodes.h | 90 ++ src/include/nodes/primnodes.h | 61 +- src/include/parser/kwlist.h | 4 + src/include/parser/parse_clause.h | 3 + src/include/utils/jsonpath.h | 3 + src/test/regress/expected/json_sqljson.out | 6 + src/test/regress/expected/jsonb_sqljson.out | 1069 +++++++++++++++++++ src/test/regress/sql/json_sqljson.sql | 4 + src/test/regress/sql/jsonb_sqljson.sql | 629 +++++++++++ src/tools/pgindent/typedefs.list | 13 + 28 files changed, 4486 insertions(+), 33 deletions(-) create mode 100644 src/backend/parser/parse_jsontable.c diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index cb36e1463b..44f016369e 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -17144,6 +17144,454 @@ array w/o UK? | t </sect2> + <sect2 id="functions-sqljson-table"> + <title>JSON_TABLE</title> + <indexterm> + <primary>json_table</primary> + </indexterm> + + <para> + <function>json_table</function> is an SQL/JSON function which + queries <acronym>JSON</acronym> data + and presents the results as a relational view, which can be accessed as a + regular SQL table. You can only use <function>json_table</function> inside the + <literal>FROM</literal> clause of a <literal>SELECT</literal> statement. + </para> + + <para> + Taking JSON data as input, <function>json_table</function> uses + a path expression to extract a part of the provided data that + will be used as a <firstterm>row pattern</firstterm> for the + constructed view. Each SQL/JSON item at the top level of the row pattern serves + as the source for a separate row in the constructed relational view. + </para> + + <para> + To split the row pattern into columns, <function>json_table</function> + provides the <literal>COLUMNS</literal> clause that defines the + schema of the created view. For each column to be constructed, + this clause provides a separate path expression that evaluates + the row pattern, extracts a JSON item, and returns it as a + separate SQL value for the specified column. If the required value + is stored in a nested level of the row pattern, it can be extracted + using the <literal>NESTED PATH</literal> subclause. Joining the + columns returned by <literal>NESTED PATH</literal> can add multiple + new rows to the constructed view. Such rows are called + <firstterm>child rows</firstterm>, as opposed to the <firstterm>parent row</firstterm> + that generates them. + </para> + + <para> + The rows produced by <function>JSON_TABLE</function> are laterally + joined to the row that generated them, so you do not have to explicitly join + the constructed view with the original table holding <acronym>JSON</acronym> + data. Optionally, you can specify how to join the columns returned + by <literal>NESTED PATH</literal> using the <literal>PLAN</literal> clause. + </para> + + <para> + Each <literal>NESTED PATH</literal> clause can generate one or more + columns. Columns produced by <literal>NESTED PATH</literal>s at the + same level are considered to be <firstterm>siblings</firstterm>, + while a column produced by a <literal>NESTED PATH</literal> is + considered to be a child of the column produced by a + <literal>NESTED PATH</literal> or row expression at a higher level. + Sibling columns are always joined first. Once they are processed, + the resulting rows are joined to the parent row. + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional></literal> + </term> + <listitem> + <para> + The input data to query, the JSON path expression defining the query, + and an optional <literal>PASSING</literal> clause, which can provide data + values to the <replaceable>path_expression</replaceable>. + The result of the input data + evaluation is called the <firstterm>row pattern</firstterm>. The row + pattern is used as the source for row values in the constructed view. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>COLUMNS</literal>( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + The <literal>COLUMNS</literal> clause defining the schema of the + constructed view. In this clause, you must specify all the columns + to be filled with SQL/JSON items. + The <replaceable>json_table_column</replaceable> + expression has the following syntax variants: + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>name</replaceable> <replaceable>type</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional></literal> + </term> + <listitem> + + <para> + Inserts a single SQL/JSON item into each row of + the specified column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable> + and fills the column with produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>ON EMPTY</literal> and + <literal>ON ERROR</literal> clauses to define how to handle missing values + or structural errors. + <literal>WRAPPER</literal> and <literal>QUOTES</literal> clauses can only + be used with JSON, array, and composite types. + These clauses have the same syntax and semantics as for + <function>json_value</function> and <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> <literal>FORMAT</literal> <replaceable>json_representation</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a composite SQL/JSON + item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable> + and fills the column with produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>WRAPPER</literal>, <literal>QUOTES</literal>, + <literal>ON EMPTY</literal> and <literal>ON ERROR</literal> clauses + to define additional settings for the returned SQL/JSON items. + These clauses have the same syntax and semantics as + for <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> + <literal>EXISTS</literal> <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a boolean item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable>, + checks whether any SQL/JSON items were returned, and fills the column with + resulting boolean value, one for each row. + The specified <replaceable>type</replaceable> should have cast from + <type>boolean</type>. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + </para> + <para> + Optionally, you can add <literal>ON ERROR</literal> clause to define + error behavior. This clause has the same syntax and semantics as + for <function>json_exists</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>NESTED PATH</literal> <replaceable>json_path_specification</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> + <literal>COLUMNS</literal> ( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + Extracts SQL/JSON items from nested levels of the row pattern, + generates one or more columns as defined by the <literal>COLUMNS</literal> + subclause, and inserts the extracted SQL/JSON items into each row of these columns. + The <replaceable>json_table_column</replaceable> expression in the + <literal>COLUMNS</literal> subclause uses the same syntax as in the + parent <literal>COLUMNS</literal> clause. + </para> + + <para> + The <literal>NESTED PATH</literal> syntax is recursive, + so you can go down multiple nested levels by specifying several + <literal>NESTED PATH</literal> subclauses within each other. + It allows to unnest the hierarchy of JSON objects and arrays + in a single function invocation rather than chaining several + <function>JSON_TABLE</function> expressions in an SQL statement. + </para> + + <para> + You can use the <literal>PLAN</literal> clause to define how + to join the columns returned by <literal>NESTED PATH</literal> clauses. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <literal>FOR ORDINALITY</literal> + </term> + <listitem> + + <para> + Adds an ordinality column that provides sequential row numbering. + You can have only one ordinality column per table. Row numbering + is 1-based. For child rows that result from the <literal>NESTED PATH</literal> + clauses, the parent row number is repeated. + </para> + </listitem> + </varlistentry> + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>AS</literal> <replaceable>json_path_name</replaceable> + </term> + <listitem> + + <para> + The optional <replaceable>json_path_name</replaceable> serves as an + identifier of the provided <replaceable>json_path_specification</replaceable>. + The path name must be unique and distinct from the column names. + When using the <literal>PLAN</literal> clause, you must specify the names + for all the paths, including the row pattern. Each path name can appear in + the <literal>PLAN</literal> clause only once. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN</literal> ( <replaceable>json_table_plan</replaceable> ) + </term> + <listitem> + + <para> + Defines how to join the data returned by <literal>NESTED PATH</literal> + clauses to the constructed view. + </para> + <para> + To join columns with parent/child relationship, you can use: + </para> + <variablelist> + <varlistentry> + <term> + <literal>INNER</literal> + </term> + <listitem> + + <para> + Use <literal>INNER JOIN</literal>, so that the parent row + is omitted from the output if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>OUTER</literal> + </term> + <listitem> + + <para> + Use <literal>LEFT OUTER JOIN</literal>, so that the parent row + is always included into the output even if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>, with NULL values + inserted into the child columns if the corresponding + values are missing. + </para> + <para> + This is the default option for joining columns with parent/child relationship. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para> + To join sibling columns, you can use: + </para> + + <variablelist> + <varlistentry> + <term> + <literal>UNION</literal> + </term> + <listitem> + + <para> + Generate one row for each value produced by each of the sibling + columns. The columns from the other siblings are set to null. + </para> + <para> + This is the default option for joining sibling columns. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>CROSS</literal> + </term> + <listitem> + + <para> + Generate one row for each combination of values from the sibling columns. + </para> + </listitem> + </varlistentry> + + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN DEFAULT</literal> ( <literal><replaceable>OUTER | INNER</replaceable> <optional>, <replaceable>UNION | CROSS</replaceable> </optional></literal> ) + </term> + <listitem> + <para> + The terms can also be specified in reverse order. The + <literal>INNER</literal> or <literal>OUTER</literal> option defines the + joining plan for parent/child columns, while <literal>UNION</literal> or + <literal>CROSS</literal> affects joins of sibling columns. This form + of <literal>PLAN</literal> overrides the default plan for + all columns at once. Even though the path names are not included in the + <literal>PLAN DEFAULT</literal> form, to conform to the SQL/JSON standard + they must be provided for all the paths if the <literal>PLAN</literal> + clause is used. + </para> + <para> + <literal>PLAN DEFAULT</literal> is simpler than specifying a complete + <literal>PLAN</literal>, and is often all that is required to get the desired + output. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para>Examples</para> + + <para> + In these examples the following small table storing some JSON data will be used: +<programlisting> +CREATE TABLE my_films ( js jsonb ); + +INSERT INTO my_films VALUES ( +'{ "favorites" : [ + { "kind" : "comedy", "films" : [ + { "title" : "Bananas", + "director" : "Woody Allen"}, + { "title" : "The Dinner Game", + "director" : "Francis Veber" } ] }, + { "kind" : "horror", "films" : [ + { "title" : "Psycho", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "thriller", "films" : [ + { "title" : "Vertigo", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "drama", "films" : [ + { "title" : "Yojimbo", + "director" : "Akira Kurosawa" } ] } + ] }'); +</programlisting> + </para> + <para> + Query the <structname>my_films</structname> table holding + some JSON data about the films and create a view that + distributes the film genre, title, and director between separate columns: +<screen> +SELECT jt.* FROM + my_films, + JSON_TABLE ( js, '$.favorites[*]' COLUMNS ( + id FOR ORDINALITY, + kind text PATH '$.kind', + NESTED PATH '$.films[*]' COLUMNS ( + title text PATH '$.title', + director text PATH '$.director'))) AS jt; +----+----------+------------------+------------------- + id | kind | title | director +----+----------+------------------+------------------- + 1 | comedy | Bananas | Woody Allen + 1 | comedy | The Dinner Game | Francis Veber + 2 | horror | Psycho | Alfred Hitchcock + 3 | thriller | Vertigo | Alfred Hitchcock + 4 | drama | Yojimbo | Akira Kurosawa + (5 rows) +</screen> + </para> + + <para> + Find a director that has done films in two different genres: +<screen> +SELECT + director1 AS director, title1, kind1, title2, kind2 +FROM + my_films, + JSON_TABLE ( js, '$.favorites' AS favs COLUMNS ( + NESTED PATH '$[*]' AS films1 COLUMNS ( + kind1 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film1 COLUMNS ( + title1 text PATH '$.title', + director1 text PATH '$.director') + ), + NESTED PATH '$[*]' AS films2 COLUMNS ( + kind2 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film2 COLUMNS ( + title2 text PATH '$.title', + director2 text PATH '$.director' + ) + ) + ) + PLAN (favs OUTER ((films1 INNER film1) CROSS (films2 INNER film2))) + ) AS jt + WHERE kind1 > kind2 AND director1 = director2; + + director | title1 | kind1 | title2 | kind2 +------------------+---------+----------+--------+-------- + Alfred Hitchcock | Vertigo | thriller | Psycho | horror +(1 row) +</screen> + </para> + </sect2> + <sect2 id="functions-sqljson-path"> <title>The SQL/JSON Path Language</title> diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 8570b14f62..ad899de5d6 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -3862,7 +3862,13 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es) break; case T_TableFuncScan: Assert(rte->rtekind == RTE_TABLEFUNC); - objectname = "xmltable"; + if (rte->tablefunc) + if (rte->tablefunc->functype == TFT_XMLTABLE) + objectname = "xmltable"; + else /* Must be TFT_JSON_TABLE */ + objectname = "json_table"; + else + objectname = NULL; objecttag = "Table Function Name"; break; case T_ValuesScan: diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 4c97e714ea..84c8730129 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -4309,6 +4309,11 @@ ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) break; } + case JSON_TABLE_OP: + res = item; + resnull = false; + break; + default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); *op->resnull = true; diff --git a/src/backend/executor/nodeTableFuncscan.c b/src/backend/executor/nodeTableFuncscan.c index 791cbd2372..085a612533 100644 --- a/src/backend/executor/nodeTableFuncscan.c +++ b/src/backend/executor/nodeTableFuncscan.c @@ -28,6 +28,7 @@ #include "miscadmin.h" #include "nodes/execnodes.h" #include "utils/builtins.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/xml.h" @@ -161,8 +162,9 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) scanstate->ss.ps.qual = ExecInitQual(node->scan.plan.qual, &scanstate->ss.ps); - /* Only XMLTABLE is supported currently */ - scanstate->routine = &XmlTableRoutine; + /* Only XMLTABLE and JSON_TABLE are supported currently */ + scanstate->routine = + tf->functype == TFT_XMLTABLE ? &XmlTableRoutine : &JsonbTableRoutine; scanstate->perTableCxt = AllocSetContextCreate(CurrentMemoryContext, @@ -182,6 +184,10 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) ExecInitExprList(tf->colexprs, (PlanState *) scanstate); scanstate->coldefexprs = ExecInitExprList(tf->coldefexprs, (PlanState *) scanstate); + scanstate->colvalexprs = + ExecInitExprList(tf->colvalexprs, (PlanState *) scanstate); + scanstate->passingvalexprs = + ExecInitExprList(tf->passingvalexprs, (PlanState *) scanstate); scanstate->notnulls = tf->notnulls; @@ -381,14 +387,17 @@ tfuncInitialize(TableFuncScanState *tstate, ExprContext *econtext, Datum doc) routine->SetNamespace(tstate, ns_name, ns_uri); } - /* Install the row filter expression into the table builder context */ - value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); - if (isnull) - ereport(ERROR, - (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), - errmsg("row filter expression must not be null"))); + if (routine->SetRowFilter) + { + /* Install the row filter expression into the table builder context */ + value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); + if (isnull) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("row filter expression must not be null"))); - routine->SetRowFilter(tstate, TextDatumGetCString(value)); + routine->SetRowFilter(tstate, TextDatumGetCString(value)); + } /* * Install the column filter expressions into the table builder context. diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 7bcc12b04f..7a03283713 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -878,6 +878,25 @@ makeJsonBehavior(JsonBehaviorType type, Node *default_expr) return behavior; } +/* + * makeJsonTableJoinedPlan - + * creates a joined JsonTablePlan node + */ +Node * +makeJsonTableJoinedPlan(JsonTablePlanJoinType type, Node *plan1, Node *plan2, + int location) +{ + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_JOINED; + n->join_type = type; + n->plan1 = castNode(JsonTablePlan, plan1); + n->plan2 = castNode(JsonTablePlan, plan2); + n->location = location; + + return (Node *) n; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index d31371bb4d..e07c066e15 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -2631,6 +2631,10 @@ expression_tree_walker_impl(Node *node, return true; if (WALK(tf->coldefexprs)) return true; + if (WALK(tf->colvalexprs)) + return true; + if (WALK(tf->passingvalexprs)) + return true; } break; default: @@ -3699,6 +3703,8 @@ expression_tree_mutator_impl(Node *node, MUTATE(newnode->rowexpr, tf->rowexpr, Node *); MUTATE(newnode->colexprs, tf->colexprs, List *); MUTATE(newnode->coldefexprs, tf->coldefexprs, List *); + MUTATE(newnode->colvalexprs, tf->colvalexprs, List *); + MUTATE(newnode->passingvalexprs, tf->passingvalexprs, List *); return (Node *) newnode; } break; @@ -4130,6 +4136,30 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonTable: + { + JsonTable *jt = (JsonTable *) node; + + if (WALK(jt->common)) + return true; + if (WALK(jt->columns)) + return true; + } + break; + case T_JsonTableColumn: + { + JsonTableColumn *jtc = (JsonTableColumn *) node; + + if (WALK(jtc->typeName)) + return true; + if (WALK(jtc->on_empty)) + return true; + if (WALK(jtc->on_error)) + return true; + if (jtc->coltype == JTC_NESTED && WALK(jtc->columns)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile index 9f1c4022bb..f4c0cc7f10 100644 --- a/src/backend/parser/Makefile +++ b/src/backend/parser/Makefile @@ -23,6 +23,7 @@ OBJS = \ parse_enr.o \ parse_expr.o \ parse_func.o \ + parse_jsontable.o \ parse_merge.o \ parse_node.o \ parse_oper.o \ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index f7ad8f18de..ff1d3332c1 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -654,19 +654,43 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_aggregate_func json_api_common_syntax json_argument + json_table + json_table_column_definition + json_table_ordinality_column_definition + json_table_regular_column_definition + json_table_formatted_column_definition + json_table_exists_column_definition + json_table_nested_columns + json_table_plan_clause_opt + json_table_plan + json_table_plan_simple + json_table_plan_parent_child + json_table_plan_outer + json_table_plan_inner + json_table_plan_sibling + json_table_plan_union + json_table_plan_cross + json_table_plan_primary %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt json_arguments + json_table_columns_clause + json_table_column_definition_list +%type <str> json_table_column_path_specification_clause_opt %type <ival> json_encoding_clause_opt json_predicate_type_constraint json_wrapper_behavior + json_table_default_plan_choices + json_table_default_plan_inner_outer + json_table_default_plan_union_cross %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt %type <jsbehavior> json_value_behavior json_query_behavior json_exists_behavior + json_table_behavior %type <js_quotes> json_quotes_clause_opt /* @@ -732,7 +756,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG - JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_TABLE JSON_VALUE KEY KEYS KEEP @@ -743,8 +767,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE - NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE - NORMALIZE NORMALIZED + NAME_P NAMES NATIONAL NATURAL NCHAR NESTED NEW NEXT NFC NFD NFKC NFKD NO + NONE NORMALIZE NORMALIZED NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC @@ -752,8 +776,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER - PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD - PLACING PLANS POLICY + PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH + PLACING PLAN PLANS POLICY POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION @@ -861,6 +885,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); * Using the same precedence as IDENT seems right for the reasons given above. */ %nonassoc UNBOUNDED /* ideally would have same precedence as IDENT */ +%nonassoc COLUMNS %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' @@ -883,6 +908,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL +%nonassoc json_table_column +%nonassoc NESTED +%left PATH %% /* @@ -13324,6 +13352,21 @@ table_ref: relation_expr opt_alias_clause $2->alias = $4; $$ = (Node *) $2; } + | json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $1); + + jt->alias = $2; + $$ = (Node *) jt; + } + | LATERAL_P json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $2); + + jt->alias = $3; + jt->lateral = true; + $$ = (Node *) jt; + } ; @@ -13891,6 +13934,8 @@ xmltable_column_option_el: { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(true), @1); } | NULL_P { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(false), @1); } + | PATH b_expr + { $$ = makeDefElem("path", $2, @1); } ; xml_namespace_list: @@ -16697,6 +16742,11 @@ json_value_behavior: | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } ; +json_table_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + ; + /* ARRAY is a noise word */ json_wrapper_behavior: WITHOUT WRAPPER { $$ = JSW_NONE; } @@ -16718,6 +16768,414 @@ json_quotes_clause_opt: | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } ; +json_table: + JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + json_table_behavior ON ERROR_P + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_columns_clause: + COLUMNS '(' json_table_column_definition_list ')' { $$ = $3; } + ; + +json_table_column_definition_list: + json_table_column_definition + { $$ = list_make1($1); } + | json_table_column_definition_list ',' json_table_column_definition + { $$ = lappend($1, $3); } + ; + +json_table_column_definition: + json_table_ordinality_column_definition %prec json_table_column + | json_table_regular_column_definition %prec json_table_column + | json_table_formatted_column_definition %prec json_table_column + | json_table_exists_column_definition %prec json_table_column + | json_table_nested_columns + ; + +json_table_ordinality_column_definition: + ColId FOR ORDINALITY + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FOR_ORDINALITY; + n->name = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_regular_column_definition: + ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_exists_column_definition: + ColId Typename + EXISTS json_table_column_path_specification_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + EXISTS json_table_column_path_specification_clause_opt + json_exists_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_column_path_specification_clause_opt: + PATH Sconst { $$ = $2; } + | /* EMPTY */ %prec json_table_column { $$ = NULL; } + ; + +json_table_formatted_column_definition: + ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->on_error = $12; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_nested_columns: + NESTED path_opt Sconst + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = NULL; + n->columns = $4; + n->location = @1; + $$ = (Node *) n; + } + | NESTED path_opt Sconst AS name + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = $5; + n->columns = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +path_opt: + PATH { } + | /* EMPTY */ { } + ; + +json_table_plan_clause_opt: + PLAN '(' json_table_plan ')' { $$ = $3; } + | PLAN DEFAULT '(' json_table_default_plan_choices ')' + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_DEFAULT; + n->join_type = $4; + n->location = @1; + $$ = (Node *) n; + } + | /* EMPTY */ { $$ = NULL; } + ; + +json_table_plan: + json_table_plan_simple + | json_table_plan_parent_child + | json_table_plan_sibling + ; + +json_table_plan_simple: + name + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_SIMPLE; + n->pathname = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_plan_primary: + json_table_plan_simple { $$ = $1; } + | '(' json_table_plan ')' + { + castNode(JsonTablePlan, $2)->location = @1; + $$ = $2; + } + ; + +json_table_plan_outer: + json_table_plan_simple OUTER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_OUTER, $1, $3, @1); } + ; + +json_table_plan_inner: + json_table_plan_simple INNER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_INNER, $1, $3, @1); } + ; + +json_table_plan_parent_child: + json_table_plan_outer + | json_table_plan_inner + ; + +json_table_plan_union: + json_table_plan_primary UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + | json_table_plan_union UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + ; + +json_table_plan_cross: + json_table_plan_primary CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + | json_table_plan_cross CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + ; + +json_table_plan_sibling: + json_table_plan_union + | json_table_plan_cross + ; + +json_table_default_plan_choices: + json_table_default_plan_inner_outer { $$ = $1 | JSTPJ_UNION; } + | json_table_default_plan_union_cross { $$ = $1 | JSTPJ_OUTER; } + | json_table_default_plan_inner_outer ',' + json_table_default_plan_union_cross { $$ = $1 | $3; } + | json_table_default_plan_union_cross ',' + json_table_default_plan_inner_outer { $$ = $1 | $3; } + ; + +json_table_default_plan_inner_outer: + INNER_P { $$ = JSTPJ_INNER; } + | OUTER_P { $$ = JSTPJ_OUTER; } + ; + +json_table_default_plan_union_cross: + UNION { $$ = JSTPJ_UNION; } + | CROSS { $$ = JSTPJ_CROSS; } + ; + json_returning_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17442,6 +17900,7 @@ unreserved_keyword: | MOVE | NAME_P | NAMES + | NESTED | NEW | NEXT | NFC @@ -17476,6 +17935,8 @@ unreserved_keyword: | PARTITION | PASSING | PASSWORD + | PATH + | PLAN | PLANS | POLICY | PRECEDING @@ -17640,6 +18101,7 @@ col_name_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | LEAST | NATIONAL @@ -18008,6 +18470,7 @@ bare_label_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | KEEP | KEY @@ -18047,6 +18510,7 @@ bare_label_keyword: | NATIONAL | NATURAL | NCHAR + | NESTED | NEW | NEXT | NFC @@ -18091,7 +18555,9 @@ bare_label_keyword: | PARTITION | PASSING | PASSWORD + | PATH | PLACING + | PLAN | PLANS | POLICY | POSITION diff --git a/src/backend/parser/meson.build b/src/backend/parser/meson.build index 94517a7f7d..0f0e4ac7ca 100644 --- a/src/backend/parser/meson.build +++ b/src/backend/parser/meson.build @@ -10,6 +10,7 @@ backend_sources += files( 'parse_enr.c', 'parse_expr.c', 'parse_func.c', + 'parse_jsontable.c', 'parse_merge.c', 'parse_node.c', 'parse_oper.c', diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index f61f794755..b44ff44991 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -697,7 +697,11 @@ transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf) char **names; int colno; - /* Currently only XMLTABLE is supported */ + /* + * Currently we only support XMLTABLE here. See transformJsonTable() for + * JSON_TABLE support. + */ + tf->functype = TFT_XMLTABLE; constructName = "XMLTABLE"; docType = XMLOID; @@ -1104,13 +1108,17 @@ transformFromClauseItem(ParseState *pstate, Node *n, rtr->rtindex = nsitem->p_rtindex; return (Node *) rtr; } - else if (IsA(n, RangeTableFunc)) + else if (IsA(n, RangeTableFunc) || IsA(n, JsonTable)) { /* table function is like a plain relation */ RangeTblRef *rtr; ParseNamespaceItem *nsitem; - nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + if (IsA(n, RangeTableFunc)) + nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + else + nsitem = transformJsonTable(pstate, (JsonTable *) n); + *top_nsitem = nsitem; *namespace = list_make1(nsitem); rtr = makeNode(RangeTblRef); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 76ce351ecf..d0b6541a09 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4252,7 +4252,7 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) JsonFormatType format; char *constructName; - if (func->common->pathname) + if (func->common->pathname && func->op != JSON_TABLE_OP) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("JSON_TABLE path name is not allowed here"), @@ -4271,6 +4271,9 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) case JSON_EXISTS_OP: constructName = "JSON_EXISTS()"; break; + case JSON_TABLE_OP: + constructName = "JSON_TABLE()"; + break; default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); break; @@ -4310,14 +4313,19 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) &jsexpr->passing_values, &jsexpr->passing_names); - if (func->op != JSON_EXISTS_OP) + if (func->op != JSON_EXISTS_OP && func->op != JSON_TABLE_OP) jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, JSON_BEHAVIOR_NULL); - jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, - func->op == JSON_EXISTS_OP ? - JSON_BEHAVIOR_FALSE : - JSON_BEHAVIOR_NULL); + if (func->op == JSON_EXISTS_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_FALSE); + else if (func->op == JSON_TABLE_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_EMPTY); + else + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_NULL); return jsexpr; } @@ -4641,6 +4649,21 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->result_coercion->expr = NULL; } break; + + case JSON_TABLE_OP: + jsexpr->returning = makeNode(JsonReturning); + jsexpr->returning->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + jsexpr->returning->typid = exprType(contextItemExpr); + jsexpr->returning->typmod = -1; + + if (jsexpr->returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("JSON_TABLE() is not yet implemented for the json type"), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + break; } if (exprType(contextItemExpr) != JSONBOID) diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c new file mode 100644 index 0000000000..a7802e0499 --- /dev/null +++ b/src/backend/parser/parse_jsontable.c @@ -0,0 +1,739 @@ +/*------------------------------------------------------------------------- + * + * parse_jsontable.c + * parsing of JSON_TABLE + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/parser/parse_jsontable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/pg_collation.h" +#include "catalog/pg_type.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "optimizer/optimizer.h" +#include "parser/parse_clause.h" +#include "parser/parse_collate.h" +#include "parser/parse_expr.h" +#include "parser/parse_relation.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/json.h" +#include "utils/lsyscache.h" + +/* Context for JSON_TABLE transformation */ +typedef struct JsonTableParseContext +{ + ParseState *pstate; /* parsing state */ + JsonTable *table; /* untransformed node */ + TableFunc *tablefunc; /* transformed node */ + List *pathNames; /* list of all path and columns names */ + int pathNameId; /* path name id counter */ + Oid contextItemTypid; /* type oid of context item (json/jsonb) */ +} JsonTableParseContext; + +static JsonTableParent *transformJsonTableColumns(JsonTableParseContext *cxt, + JsonTablePlan *plan, + List *columns, + char *pathSpec, + char **pathName, + int location); + +static Node * +makeStringConst(char *str, int location) +{ + A_Const *n = makeNode(A_Const); + + n->val.node.type = T_String; + n->val.sval.sval = str; + n->location = location; + + return (Node *) n; +} + +/* + * Transform JSON_TABLE column + * - regular column into JSON_VALUE() + * - FORMAT JSON column into JSON_QUERY() + * - EXISTS column into JSON_EXISTS() + */ +static Node * +transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, + List *passingArgs, bool errorOnError) +{ + JsonFuncExpr *jfexpr = makeNode(JsonFuncExpr); + JsonCommon *common = makeNode(JsonCommon); + JsonOutput *output = makeNode(JsonOutput); + char *pathspec; + JsonFormat *default_format; + + jfexpr->op = + jtc->coltype == JTC_REGULAR ? JSON_VALUE_OP : + jtc->coltype == JTC_EXISTS ? JSON_EXISTS_OP : JSON_QUERY_OP; + jfexpr->common = common; + jfexpr->output = output; + jfexpr->on_empty = jtc->on_empty; + jfexpr->on_error = jtc->on_error; + if (!jfexpr->on_error && errorOnError) + jfexpr->on_error = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); + jfexpr->omit_quotes = jtc->omit_quotes; + jfexpr->wrapper = jtc->wrapper; + jfexpr->location = jtc->location; + + output->typeName = jtc->typeName; + output->returning = makeNode(JsonReturning); + output->returning->format = jtc->format; + + default_format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + + common->pathname = NULL; + common->expr = makeJsonValueExpr((Expr *) contextItemExpr, default_format); + common->passing = passingArgs; + + if (jtc->pathspec) + pathspec = jtc->pathspec; + else + { + /* Construct default path as '$."column_name"' */ + StringInfoData path; + + initStringInfo(&path); + + appendStringInfoString(&path, "$."); + escape_json(&path, jtc->name); + + pathspec = path.data; + } + + common->pathspec = makeStringConst(pathspec, -1); + + return (Node *) jfexpr; +} + +static bool +isJsonTablePathNameDuplicate(JsonTableParseContext *cxt, const char *pathname) +{ + ListCell *lc; + + foreach(lc, cxt->pathNames) + { + if (!strcmp(pathname, (const char *) lfirst(lc))) + return true; + } + + return false; +} + +/* Register the column name in the path name list. */ +static void +registerJsonTableColumn(JsonTableParseContext *cxt, char *colname) +{ + if (isJsonTablePathNameDuplicate(cxt, colname)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_ALIAS), + errmsg("duplicate JSON_TABLE column name: %s", colname), + errhint("JSON_TABLE column names must be distinct from one another."))); + + cxt->pathNames = lappend(cxt->pathNames, colname); +} + +/* Recursively register all nested column names in the path name list. */ +static void +registerAllJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED) + { + if (jtc->pathname) + registerJsonTableColumn(cxt, jtc->pathname); + + registerAllJsonTableColumns(cxt, jtc->columns); + } + else + { + registerJsonTableColumn(cxt, jtc->name); + } + } +} + +/* Generate a new unique JSON_TABLE path name. */ +static char * +generateJsonTablePathName(JsonTableParseContext *cxt) +{ + char namebuf[32]; + char *name = namebuf; + + do + { + snprintf(namebuf, sizeof(namebuf), "json_table_path_%d", + ++cxt->pathNameId); + } while (isJsonTablePathNameDuplicate(cxt, name)); + + name = pstrdup(name); + cxt->pathNames = lappend(cxt->pathNames, name); + + return name; +} + +/* Collect sibling path names from plan to the specified list. */ +static void +collectSiblingPathsInJsonTablePlan(JsonTablePlan *plan, List **paths) +{ + if (plan->plan_type == JSTP_SIMPLE) + *paths = lappend(*paths, plan->pathname); + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + *paths = lappend(*paths, plan->plan1->pathname); + } + else if (plan->join_type == JSTPJ_CROSS || + plan->join_type == JSTPJ_UNION) + { + collectSiblingPathsInJsonTablePlan(plan->plan1, paths); + collectSiblingPathsInJsonTablePlan(plan->plan2, paths); + } + else + elog(ERROR, "invalid JSON_TABLE join type %d", + plan->join_type); + } +} + +/* + * Validate child JSON_TABLE plan by checking that: + * - all nested columns have path names specified + * - all nested columns have corresponding node in the sibling plan + * - plan does not contain duplicate or extra nodes + */ +static void +validateJsonTableChildPlan(ParseState *pstate, JsonTablePlan *plan, + List *columns) +{ + ListCell *lc1; + List *siblings = NIL; + int nchildren = 0; + + if (plan) + collectSiblingPathsInJsonTablePlan(plan, &siblings); + + foreach(lc1, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc1)); + + if (jtc->coltype == JTC_NESTED) + { + ListCell *lc2; + bool found = false; + + if (!jtc->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("nested JSON_TABLE columns must contain an explicit AS pathname specification if an explicit PLAN clause is used"), + parser_errposition(pstate, jtc->location))); + + /* find nested path name in the list of sibling path names */ + foreach(lc2, siblings) + { + if ((found = !strcmp(jtc->pathname, lfirst(lc2)))) + break; + } + + if (!found) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node for nested path %s was not found in plan.", jtc->pathname), + parser_errposition(pstate, jtc->location))); + + nchildren++; + } + } + + if (list_length(siblings) > nchildren) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node contains some extra or duplicate sibling nodes."), + parser_errposition(pstate, plan ? plan->location : -1))); +} + +static JsonTableColumn * +findNestedJsonTableColumn(List *columns, const char *pathname) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED && + jtc->pathname && + !strcmp(jtc->pathname, pathname)) + return jtc; + } + + return NULL; +} + +static Node * +transformNestedJsonTableColumn(JsonTableParseContext *cxt, JsonTableColumn *jtc, + JsonTablePlan *plan) +{ + JsonTableParent *node; + char *pathname = jtc->pathname; + + node = transformJsonTableColumns(cxt, plan, jtc->columns, jtc->pathspec, + &pathname, jtc->location); + + return (Node *) node; +} + +static Node * +makeJsonTableSiblingJoin(bool cross, Node *lnode, Node *rnode) +{ + JsonTableSibling *join = makeNode(JsonTableSibling); + + join->larg = lnode; + join->rarg = rnode; + join->cross = cross; + + return (Node *) join; +} + +/* + * Recursively transform child JSON_TABLE plan. + * + * Default plan is transformed into a cross/union join of its nested columns. + * Simple and outer/inner plans are transformed into a JsonTableParent by + * finding and transforming corresponding nested column. + * Sibling plans are recursively transformed into a JsonTableSibling. + */ +static Node * +transformJsonTableChildPlan(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns) +{ + JsonTableColumn *jtc = NULL; + + if (!plan || plan->plan_type == JSTP_DEFAULT) + { + /* unspecified or default plan */ + Node *res = NULL; + ListCell *lc; + bool cross = plan && (plan->join_type & JSTPJ_CROSS); + + /* transform all nested columns into cross/union join */ + foreach(lc, columns) + { + JsonTableColumn *col = castNode(JsonTableColumn, lfirst(lc)); + Node *node; + + if (col->coltype != JTC_NESTED) + continue; + + node = transformNestedJsonTableColumn(cxt, col, plan); + + /* join transformed node with previous sibling nodes */ + res = res ? makeJsonTableSiblingJoin(cross, res, node) : node; + } + + return res; + } + else if (plan->plan_type == JSTP_SIMPLE) + { + jtc = findNestedJsonTableColumn(columns, plan->pathname); + } + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + jtc = findNestedJsonTableColumn(columns, plan->plan1->pathname); + } + else + { + Node *node1 = transformJsonTableChildPlan(cxt, plan->plan1, + columns); + Node *node2 = transformJsonTableChildPlan(cxt, plan->plan2, + columns); + + return makeJsonTableSiblingJoin(plan->join_type == JSTPJ_CROSS, + node1, node2); + } + } + else + elog(ERROR, "invalid JSON_TABLE plan type %d", plan->plan_type); + + if (!jtc) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name was %s not found in nested columns list.", + plan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + return transformNestedJsonTableColumn(cxt, jtc, plan); +} + +/* Check whether type is json/jsonb, array, or record. */ +static bool +typeIsComposite(Oid typid) +{ + char typtype; + + if (typid == JSONOID || + typid == JSONBOID || + typid == RECORDOID || + type_is_array(typid)) + return true; + + typtype = get_typtype(typid); + + if (typtype == TYPTYPE_COMPOSITE) + return true; + + if (typtype == TYPTYPE_DOMAIN) + return typeIsComposite(getBaseType(typid)); + + return false; +} + +/* Append transformed non-nested JSON_TABLE columns to the TableFunc node */ +static void +appendJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *col; + ParseState *pstate = cxt->pstate; + JsonTable *jt = cxt->table; + TableFunc *tf = cxt->tablefunc; + bool errorOnError = jt->on_error && + jt->on_error->btype == JSON_BEHAVIOR_ERROR; + + foreach(col, columns) + { + JsonTableColumn *rawc = castNode(JsonTableColumn, lfirst(col)); + Oid typid; + int32 typmod; + Node *colexpr; + + if (rawc->name) + { + /* make sure column names are unique */ + ListCell *colname; + + foreach(colname, tf->colnames) + if (!strcmp((const char *) colname, rawc->name)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("column name \"%s\" is not unique", + rawc->name), + parser_errposition(pstate, rawc->location))); + + tf->colnames = lappend(tf->colnames, + makeString(pstrdup(rawc->name))); + } + + /* + * Determine the type and typmod for the new column. FOR ORDINALITY + * columns are INTEGER by standard; the others are user-specified. + */ + switch (rawc->coltype) + { + case JTC_FOR_ORDINALITY: + colexpr = NULL; + typid = INT4OID; + typmod = -1; + break; + + case JTC_REGULAR: + typenameTypeIdAndMod(pstate, rawc->typeName, &typid, &typmod); + + /* + * Use implicit FORMAT JSON for composite types (arrays and + * records) + */ + if (typeIsComposite(typid)) + rawc->coltype = JTC_FORMATTED; + else if (rawc->wrapper != JSW_NONE) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use WITH WRAPPER clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + else if (rawc->omit_quotes) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use OMIT QUOTES clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + + /* FALLTHROUGH */ + case JTC_EXISTS: + case JTC_FORMATTED: + { + Node *je; + CaseTestExpr *param = makeNode(CaseTestExpr); + + param->collation = InvalidOid; + param->typeId = cxt->contextItemTypid; + param->typeMod = -1; + + je = transformJsonTableColumn(rawc, (Node *) param, + NIL, errorOnError); + + colexpr = transformExpr(pstate, je, EXPR_KIND_FROM_FUNCTION); + assign_expr_collations(pstate, colexpr); + + typid = exprType(colexpr); + typmod = exprTypmod(colexpr); + break; + } + + case JTC_NESTED: + continue; + + default: + elog(ERROR, "unknown JSON_TABLE column type: %d", rawc->coltype); + break; + } + + tf->coltypes = lappend_oid(tf->coltypes, typid); + tf->coltypmods = lappend_int(tf->coltypmods, typmod); + tf->colcollations = lappend_oid(tf->colcollations, get_typcollation(typid)); + tf->colvalexprs = lappend(tf->colvalexprs, colexpr); + } +} + +/* + * Create transformed JSON_TABLE parent plan node by appending all non-nested + * columns to the TableFunc node and remembering their indices in the + * colvalexprs list. + */ +static JsonTableParent * +makeParentJsonTableNode(JsonTableParseContext *cxt, char *pathSpec, char *pathName, + List *columns) +{ + JsonTableParent *node = makeNode(JsonTableParent); + + node->path = makeNode(JsonTablePath); + node->path->value = makeConst(JSONPATHOID, -1, InvalidOid, -1, + DirectFunctionCall1(jsonpath_in, + CStringGetDatum(pathSpec)), + false, false); + if (pathName) + node->path->name = pstrdup(pathName); + + /* save start of column range */ + node->colMin = list_length(cxt->tablefunc->colvalexprs); + + appendJsonTableColumns(cxt, columns); + + /* save end of column range */ + node->colMax = list_length(cxt->tablefunc->colvalexprs) - 1; + + node->errorOnError = + cxt->table->on_error && + cxt->table->on_error->btype == JSON_BEHAVIOR_ERROR; + + return node; +} + +static JsonTableParent * +transformJsonTableColumns(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns, char *pathSpec, char **pathName, + int location) +{ + JsonTableParent *node; + JsonTablePlan *childPlan; + bool defaultPlan = !plan || plan->plan_type == JSTP_DEFAULT; + + if (!*pathName) + { + if (cxt->table->plan) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE expression"), + errdetail("JSON_TABLE columns must contain " + "explicit AS pathname specification if " + "explicit PLAN clause is used"), + parser_errposition(cxt->pstate, location))); + + *pathName = generateJsonTablePathName(cxt); + } + + if (defaultPlan) + childPlan = plan; + else + { + /* validate parent and child plans */ + JsonTablePlan *parentPlan; + + if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type != JSTPJ_INNER && + plan->join_type != JSTPJ_OUTER) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Expected INNER or OUTER JSON_TABLE plan node."), + parser_errposition(cxt->pstate, plan->location))); + + parentPlan = plan->plan1; + childPlan = plan->plan2; + + Assert(parentPlan->plan_type != JSTP_JOINED); + Assert(parentPlan->pathname); + } + else + { + parentPlan = plan; + childPlan = NULL; + } + + if (strcmp(parentPlan->pathname, *pathName)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name mismatch: expected %s but %s is given.", + *pathName, parentPlan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + validateJsonTableChildPlan(cxt->pstate, childPlan, columns); + } + + /* transform only non-nested columns */ + node = makeParentJsonTableNode(cxt, pathSpec, *pathName, columns); + + if (childPlan || defaultPlan) + { + /* transform recursively nested columns */ + node->child = transformJsonTableChildPlan(cxt, childPlan, columns); + if (node->child) + node->outerJoin = !plan || (plan->join_type & JSTPJ_OUTER); + /* else: default plan case, no children found */ + } + + return node; +} + +/* + * transformJsonTable - + * Transform a raw JsonTable into TableFunc. + * + * Transform the document-generating expression, the row-generating expression, + * the column-generating expressions, and the default value expressions. + */ +ParseNamespaceItem * +transformJsonTable(ParseState *pstate, JsonTable *jt) +{ + JsonTableParseContext cxt; + TableFunc *tf = makeNode(TableFunc); + JsonFuncExpr *jfe = makeNode(JsonFuncExpr); + JsonExpr *je; + JsonTablePlan *plan = jt->plan; + JsonCommon *jscommon; + char *rootPathName = jt->common->pathname; + char *rootPath; + bool is_lateral; + + cxt.pstate = pstate; + cxt.table = jt; + cxt.tablefunc = tf; + cxt.pathNames = NIL; + cxt.pathNameId = 0; + + if (rootPathName) + registerJsonTableColumn(&cxt, rootPathName); + + registerAllJsonTableColumns(&cxt, jt->columns); + +#if 0 /* XXX it' unclear from the standard whether + * root path name is mandatory or not */ + if (plan && plan->plan_type != JSTP_DEFAULT && !rootPathName) + { + /* Assign root path name and create corresponding plan node */ + JsonTablePlan *rootNode = makeNode(JsonTablePlan); + JsonTablePlan *rootPlan = (JsonTablePlan *) + makeJsonTableJoinedPlan(JSTPJ_OUTER, (Node *) rootNode, + (Node *) plan, jt->location); + + rootPathName = generateJsonTablePathName(&cxt); + + rootNode->plan_type = JSTP_SIMPLE; + rootNode->pathname = rootPathName; + + plan = rootPlan; + } +#endif + + jscommon = copyObject(jt->common); + jscommon->pathspec = makeStringConst(pstrdup("$"), -1); + + jfe->op = JSON_TABLE_OP; + jfe->common = jscommon; + jfe->on_error = jt->on_error; + jfe->location = jt->common->location; + + /* + * We make lateral_only names of this level visible, whether or not the + * RangeTableFunc is explicitly marked LATERAL. This is needed for SQL + * spec compliance and seems useful on convenience grounds for all + * functions in FROM. + * + * (LATERAL can't nest within a single pstate level, so we don't need + * save/restore logic here.) + */ + Assert(!pstate->p_lateral_active); + pstate->p_lateral_active = true; + + tf->functype = TFT_JSON_TABLE; + tf->docexpr = transformExpr(pstate, (Node *) jfe, EXPR_KIND_FROM_FUNCTION); + + cxt.contextItemTypid = exprType(tf->docexpr); + + if (!IsA(jt->common->pathspec, A_Const) || + castNode(A_Const, jt->common->pathspec)->val.node.type != T_String) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("only string constants supported in JSON_TABLE path specification"), + parser_errposition(pstate, + exprLocation(jt->common->pathspec)))); + + rootPath = castNode(A_Const, jt->common->pathspec)->val.sval.sval; + + tf->plan = (Node *) transformJsonTableColumns(&cxt, plan, jt->columns, + rootPath, &rootPathName, + jt->common->location); + + /* Also save a copy of the PASSING arguments in the TableFunc node. */ + je = (JsonExpr *) tf->docexpr; + tf->passingvalexprs = copyObject(je->passing_values); + + tf->ordinalitycol = -1; /* undefine ordinality column number */ + tf->location = jt->location; + + pstate->p_lateral_active = false; + + /* + * Mark the RTE as LATERAL if the user said LATERAL explicitly, or if + * there are any lateral cross-references in it. + */ + is_lateral = jt->lateral || contain_vars_of_level((Node *) tf, 0); + + return addRangeTableEntryForTableFunc(pstate, + tf, jt->alias, is_lateral, true); +} diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 41d60494b9..7da42c4772 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -2072,7 +2072,8 @@ addRangeTableEntryForTableFunc(ParseState *pstate, Assert(list_length(tf->coltypmods) == list_length(tf->colnames)); Assert(list_length(tf->colcollations) == list_length(tf->colnames)); - refname = alias ? alias->aliasname : pstrdup("xmltable"); + refname = alias ? alias->aliasname : + pstrdup(tf->functype == TFT_XMLTABLE ? "xmltable" : "json_table"); rte->rtekind = RTE_TABLEFUNC; rte->relid = InvalidOid; @@ -2095,7 +2096,7 @@ addRangeTableEntryForTableFunc(ParseState *pstate, ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), errmsg("%s function has %d columns available but %d columns specified", - "XMLTABLE", + tf->functype == TFT_XMLTABLE ? "XMLTABLE" : "JSON_TABLE", list_length(tf->colnames), numaliases))); rte->eref = eref; diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4f94fc69d6..6500e42485 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1992,6 +1992,9 @@ FigureColnameInternal(Node *node, char **name) case JSON_EXISTS_OP: *name = "json_exists"; return 2; + case JSON_TABLE_OP: + *name = "json_table"; + return 2; } break; default: diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index eded22e194..92d76d5cde 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -61,10 +61,12 @@ #include "catalog/pg_collation.h" #include "catalog/pg_type.h" +#include "executor/execExpr.h" #include "funcapi.h" #include "lib/stringinfo.h" #include "miscadmin.h" #include "nodes/miscnodes.h" +#include "nodes/nodeFuncs.h" #include "regex/regex.h" #include "utils/builtins.h" #include "utils/date.h" @@ -75,6 +77,8 @@ #include "utils/guc.h" #include "utils/json.h" #include "utils/jsonpath.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" #include "utils/timestamp.h" #include "utils/varlena.h" @@ -156,6 +160,61 @@ typedef struct JsonValueListIterator ListCell *next; } JsonValueListIterator; +/* Structures for JSON_TABLE execution */ + +typedef enum JsonTablePlanStateType +{ + JSON_TABLE_SCAN_STATE = 0, + JSON_TABLE_JOIN_STATE +} JsonTablePlanStateType; + +typedef struct JsonTablePlanState +{ + JsonTablePlanStateType type; + + struct JsonTablePlanState *parent; + struct JsonTablePlanState *nested; +} JsonTablePlanState; + +typedef struct JsonTableScanState +{ + JsonTablePlanState plan; + + MemoryContext mcxt; + JsonPath *path; + List *args; + JsonValueList found; + JsonValueListIterator iter; + Datum current; + int ordinal; + bool currentIsNull; + bool outerJoin; + bool errorOnError; + bool advanceNested; + bool reset; +} JsonTableScanState; + +typedef struct JsonTableJoinState +{ + JsonTablePlanState plan; + + JsonTablePlanState *left; + JsonTablePlanState *right; + bool cross; + bool advanceRight; +} JsonTableJoinState; + +/* random number to identify JsonTableExecContext */ +#define JSON_TABLE_EXEC_CONTEXT_MAGIC 418352867 + +typedef struct JsonTableExecContext +{ + int magic; + JsonTableScanState **colexprscans; + JsonTableScanState *root; + bool empty; +} JsonTableExecContext; + /* strict/lax flags is decomposed into four [un]wrap/error flags */ #define jspStrictAbsenseOfErrors(cxt) (!(cxt)->laxMode) #define jspAutoUnwrap(cxt) ((cxt)->laxMode) @@ -248,6 +307,7 @@ static JsonPathExecResult getArrayIndex(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbValue *jb, int32 *index); static JsonBaseObjectInfo setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id); +static void JsonValueListClear(JsonValueList *jvl); static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv); static int JsonValueListLength(const JsonValueList *jvl); static bool JsonValueListIsEmpty(JsonValueList *jvl); @@ -265,6 +325,14 @@ static JsonbValue *wrapItemsInArray(const JsonValueList *items); static int compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, bool useTz, bool *cast_error); + +static JsonTablePlanState *JsonTableInitPlanState(JsonTableExecContext *cxt, + Node *plan, + JsonTablePlanState *parent); +static bool JsonTablePlanNextRow(JsonTablePlanState *state); +static bool JsonTableScanNextRow(JsonTableScanState *scan); + + /****************** User interface to JsonPath executor ********************/ /* @@ -2518,6 +2586,13 @@ setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id) return baseObject; } +static void +JsonValueListClear(JsonValueList *jvl) +{ + jvl->singleton = NULL; + jvl->list = NULL; +} + static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv) { @@ -3123,3 +3198,471 @@ JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); } } + +/************************ JSON_TABLE functions ***************************/ + +/* + * Returns private data from executor state. Ensure validity by check with + * MAGIC number. + */ +static inline JsonTableExecContext * +GetJsonTableExecContext(TableFuncScanState *state, const char *fname) +{ + JsonTableExecContext *result; + + if (!IsA(state, TableFuncScanState)) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + result = (JsonTableExecContext *) state->opaque; + if (result->magic != JSON_TABLE_EXEC_CONTEXT_MAGIC) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + + return result; +} + +/* Recursively initialize JSON_TABLE scan / join state */ +static JsonTableJoinState * +JsonTableInitJoinState(JsonTableExecContext *cxt, JsonTableSibling *plan, + JsonTablePlanState *parent) +{ + JsonTableJoinState *join = palloc0(sizeof(*join)); + + join->plan.type = JSON_TABLE_JOIN_STATE; + /* parent and nested not set. */ + + join->cross = plan->cross; + join->left = JsonTableInitPlanState(cxt, plan->larg, parent); + join->right = JsonTableInitPlanState(cxt, plan->rarg, parent); + + return join; +} + +static JsonTableScanState * +JsonTableInitScanState(JsonTableExecContext *cxt, JsonTableParent *plan, + JsonTablePlanState *parent, + List *args, MemoryContext mcxt) +{ + JsonTableScanState *scan = palloc0(sizeof(*scan)); + int i; + + scan->plan.type = JSON_TABLE_SCAN_STATE; + scan->plan.parent = parent; + + scan->outerJoin = plan->outerJoin; + scan->errorOnError = plan->errorOnError; + scan->path = DatumGetJsonPathP(plan->path->value->constvalue); + scan->args = args; + scan->mcxt = AllocSetContextCreate(mcxt, "JsonTableExecContext", + ALLOCSET_DEFAULT_SIZES); + + /* + * Set after settings scan->args and scan->mcxt, because the recursive call + * wants to use those values. + */ + scan->plan.nested = plan->child ? + JsonTableInitPlanState(cxt, plan->child, (JsonTablePlanState *) scan) : + NULL; + + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + + for (i = plan->colMin; i <= plan->colMax; i++) + cxt->colexprscans[i] = scan; + + return scan; +} + +/* Recursively initialize JSON_TABLE scan state */ +static JsonTablePlanState * +JsonTableInitPlanState(JsonTableExecContext *cxt, Node *plan, + JsonTablePlanState *parent) +{ + JsonTablePlanState *state; + + if (IsA(plan, JsonTableSibling)) + { + JsonTableSibling *join = castNode(JsonTableSibling, plan); + + state = (JsonTablePlanState *) + JsonTableInitJoinState(cxt, join, parent); + } + else + { + JsonTableParent *scan = castNode(JsonTableParent, plan); + JsonTableScanState *parent_scan = (JsonTableScanState *) parent; + + Assert(parent_scan); + state = (JsonTablePlanState *) + JsonTableInitScanState(cxt, scan, parent, parent_scan->args, + parent_scan->mcxt); + } + + return state; +} + +/* + * JsonTableInitOpaque + * Fill in TableFuncScanState->opaque for JsonTable processor + */ +static void +JsonTableInitOpaque(TableFuncScanState *state, int natts) +{ + JsonTableExecContext *cxt; + PlanState *ps = &state->ss.ps; + TableFuncScan *tfs = castNode(TableFuncScan, ps->plan); + TableFunc *tf = tfs->tablefunc; + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + JsonExpr *je = castNode(JsonExpr, tf->docexpr); + List *args = NIL; + + cxt = palloc0(sizeof(JsonTableExecContext)); + cxt->magic = JSON_TABLE_EXEC_CONTEXT_MAGIC; + + if (state->passingvalexprs) + { + ListCell *exprlc; + ListCell *namelc; + + Assert(list_length(state->passingvalexprs) == + list_length(je->passing_names)); + forboth(exprlc, state->passingvalexprs, + namelc, je->passing_names) + { + ExprState *state = lfirst_node(ExprState, exprlc); + String *name = lfirst_node(String, namelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(name->sval); + var->typid = exprType((Node *) state->expr); + var->typmod = exprTypmod((Node *) state->expr); + + /* + * Evaluate the expression and save the value to be returned by + * GetJsonPathVar(). + */ + var->value = ExecEvalExpr(state, ps->ps_ExprContext, + &var->isnull); + + args = lappend(args, var); + } + } + + cxt->colexprscans = palloc(sizeof(JsonTableScanState *) * + list_length(tf->colvalexprs)); + + cxt->root = JsonTableInitScanState(cxt, root, NULL, args, + CurrentMemoryContext); + state->opaque = cxt; +} + +/* Reset scan iterator to the beginning of the item list */ +static void +JsonTableRescan(JsonTableScanState *scan) +{ + JsonValueListInitIterator(&scan->found, &scan->iter); + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + scan->advanceNested = false; + scan->ordinal = 0; +} + +/* Reset context item of a scan, execute JSON path and reset a scan */ +static void +JsonTableResetContextItem(JsonTableScanState *scan, Datum item) +{ + MemoryContext oldcxt; + JsonPathExecResult res; + Jsonb *js = (Jsonb *) DatumGetJsonbP(item); + + JsonValueListClear(&scan->found); + + MemoryContextResetOnly(scan->mcxt); + + oldcxt = MemoryContextSwitchTo(scan->mcxt); + + res = executeJsonPath(scan->path, scan->args, GetJsonPathVar, js, + scan->errorOnError, &scan->found, + false /* FIXME */ ); + + MemoryContextSwitchTo(oldcxt); + + if (jperIsError(res)) + { + Assert(!scan->errorOnError); + JsonValueListClear(&scan->found); /* EMPTY ON ERROR case */ + } + + JsonTableRescan(scan); +} + +/* + * JsonTableSetDocument + * Install the input document + */ +static void +JsonTableSetDocument(TableFuncScanState *state, Datum value) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableSetDocument"); + + JsonTableResetContextItem(cxt->root, value); +} + +/* Recursively reset scan and its child nodes */ +static void +JsonTableRescanRecursive(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTableRescanRecursive(join->left); + JsonTableRescanRecursive(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan = (JsonTableScanState *) state; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + JsonTableRescan(scan); + if (scan->plan.nested) + JsonTableRescanRecursive(scan->plan.nested); + } +} + +/* + * Fetch next row from a cross/union joined scan. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTablePlanNextRow(JsonTablePlanState *state) +{ + JsonTableJoinState *join; + + if (state->type == JSON_TABLE_SCAN_STATE) + return JsonTableScanNextRow((JsonTableScanState *) state); + + join = (JsonTableJoinState *) state; + if (join->advanceRight) + { + /* fetch next inner row */ + if (JsonTablePlanNextRow(join->right)) + return true; + + /* inner rows are exhausted */ + if (join->cross) + join->advanceRight = false; /* next outer row */ + else + return false; /* end of scan */ + } + + while (!join->advanceRight) + { + /* fetch next outer row */ + bool left = JsonTablePlanNextRow(join->left); + + if (join->cross) + { + if (!left) + return false; /* end of scan */ + + JsonTableRescanRecursive(join->right); + + if (!JsonTablePlanNextRow(join->right)) + continue; /* next outer row */ + + join->advanceRight = true; /* next inner row */ + } + else if (!left) + { + if (!JsonTablePlanNextRow(join->right)) + return false; /* end of scan */ + + join->advanceRight = true; /* next inner row */ + } + + break; + } + + return true; +} + +/* Recursively set 'reset' flag of scan and its child nodes */ +static void +JsonTablePlanReset(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTablePlanReset(join->left); + JsonTablePlanReset(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + scan = (JsonTableScanState *) state; + scan->reset = true; + scan->advanceNested = false; + + if (scan->plan.nested) + JsonTablePlanReset(scan->plan.nested); + } +} + +/* + * Fetch next row from a simple scan with outer/inner joined nested subscans. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTableScanNextRow(JsonTableScanState *scan) +{ + /* reset context item if requested */ + if (scan->reset) + { + JsonTableScanState *parent_scan = + (JsonTableScanState *) scan->plan.parent; + + Assert(parent_scan && !parent_scan->currentIsNull); + JsonTableResetContextItem(scan, parent_scan->current); + scan->reset = false; + } + + if (scan->advanceNested) + { + /* fetch next nested row */ + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested) + return true; + } + + for (;;) + { + /* fetch next row */ + JsonbValue *jbv = JsonValueListNext(&scan->found, &scan->iter); + MemoryContext oldcxt; + + if (!jbv) + { + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + return false; /* end of scan */ + } + + /* set current row item */ + oldcxt = MemoryContextSwitchTo(scan->mcxt); + scan->current = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + scan->currentIsNull = false; + MemoryContextSwitchTo(oldcxt); + + scan->ordinal++; + + if (!scan->plan.nested) + break; + + JsonTablePlanReset(scan->plan.nested); + + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested || scan->outerJoin) + break; + } + + return true; +} + +/* + * JsonTableFetchRow + * Prepare the next "current" tuple for upcoming GetValue calls. + * Returns FALSE if the row-filter expression returned no more rows. + */ +static bool +JsonTableFetchRow(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableFetchRow"); + + if (cxt->empty) + return false; + + return JsonTableScanNextRow(cxt->root); +} + +/* + * JsonTableGetValue + * Return the value for column number 'colnum' for the current row. + * + * This leaks memory, so be sure to reset often the context in which it's + * called. + */ +static Datum +JsonTableGetValue(TableFuncScanState *state, int colnum, + Oid typid, int32 typmod, bool *isnull) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableGetValue"); + ExprContext *econtext = state->ss.ps.ps_ExprContext; + ExprState *estate = list_nth(state->colvalexprs, colnum); + JsonTableScanState *scan = cxt->colexprscans[colnum]; + Datum result; + + if (scan->currentIsNull) /* NULL from outer/union join */ + { + result = (Datum) 0; + *isnull = true; + } + else if (estate) /* regular column */ + { + Datum saved_caseValue = econtext->caseValue_datum; + bool saved_caseIsNull = econtext->caseValue_isNull; + + /* Pass the value for CaseTestExpr that may be present in colexpr */ + econtext->caseValue_datum = scan->current; + econtext->caseValue_isNull = false; + + result = ExecEvalExpr(estate, econtext, isnull); + + econtext->caseValue_datum = saved_caseValue; + econtext->caseValue_isNull = saved_caseIsNull; + } + else + { + result = Int32GetDatum(scan->ordinal); /* ordinality column */ + *isnull = false; + } + + return result; +} + +/* + * JsonTableDestroyOpaque + */ +static void +JsonTableDestroyOpaque(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableDestroyOpaque"); + + /* not valid anymore */ + cxt->magic = 0; + + state->opaque = NULL; +} + +const TableFuncRoutine JsonbTableRoutine = +{ + JsonTableInitOpaque, + JsonTableSetDocument, + NULL, + NULL, + NULL, + JsonTableFetchRow, + JsonTableGetValue, + JsonTableDestroyOpaque +}; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d1ec418ccf..e89d1e03d6 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -522,6 +522,8 @@ static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); static void get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit); +static void get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8606,7 +8608,8 @@ get_json_behavior(JsonBehavior *behavior, deparse_context *context, /* * get_json_expr_options * - * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS and + * JSON_TABLE columns. */ static void get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, @@ -9854,6 +9857,9 @@ get_rule_expr(Node *node, deparse_context *context, case JSON_EXISTS_OP: appendStringInfoString(buf, "JSON_EXISTS("); break; + default: + elog(ERROR, "unexpected JsonExpr type: %d", jexpr->op); + break; } get_rule_expr(jexpr->formatted_expr, context, showimplicit); @@ -11207,16 +11213,14 @@ get_sublink_expr(SubLink *sublink, deparse_context *context) /* ---------- - * get_tablefunc - Parse back a table function + * get_xmltable - Parse back a XMLTABLE function * ---------- */ static void -get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +get_xmltable(TableFunc *tf, deparse_context *context, bool showimplicit) { StringInfo buf = context->buf; - /* XMLTABLE is the only existing implementation. */ - appendStringInfoString(buf, "XMLTABLE("); if (tf->ns_uris != NIL) @@ -11307,6 +11311,271 @@ get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) appendStringInfoChar(buf, ')'); } +/* + * get_json_nested_columns - Parse back nested JSON_TABLE columns + */ +static void +get_json_table_nested_columns(TableFunc *tf, Node *node, + deparse_context *context, bool showimplicit, + bool needcomma) +{ + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_nested_columns(tf, n->larg, context, showimplicit, + needcomma); + get_json_table_nested_columns(tf, n->rarg, context, showimplicit, true); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + if (needcomma) + appendStringInfoChar(context->buf, ','); + + appendStringInfoChar(context->buf, ' '); + appendContextKeyword(context, "NESTED PATH ", 0, 0, 0); + get_const_expr(n->path->value, context, -1); + appendStringInfo(context->buf, " AS %s", quote_identifier(n->path->name)); + get_json_table_columns(tf, n, context, showimplicit); + } +} + +/* + * get_json_table_plan - Parse back a JSON_TABLE plan + */ +static void +get_json_table_plan(TableFunc *tf, Node *node, deparse_context *context, + bool parenthesize) +{ + if (parenthesize) + appendStringInfoChar(context->buf, '('); + + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_plan(tf, n->larg, context, + IsA(n->larg, JsonTableSibling) || + castNode(JsonTableParent, n->larg)->child); + + appendStringInfoString(context->buf, n->cross ? " CROSS " : " UNION "); + + get_json_table_plan(tf, n->rarg, context, + IsA(n->rarg, JsonTableSibling) || + castNode(JsonTableParent, n->rarg)->child); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + appendStringInfoString(context->buf, quote_identifier(n->path->name)); + + if (n->child) + { + appendStringInfoString(context->buf, + n->outerJoin ? " OUTER " : " INNER "); + get_json_table_plan(tf, n->child, context, + IsA(n->child, JsonTableSibling)); + } + } + + if (parenthesize) + appendStringInfoChar(context->buf, ')'); +} + +/* + * get_json_table_columns - Parse back JSON_TABLE columns + */ +static void +get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + ListCell *lc_colname; + ListCell *lc_coltype; + ListCell *lc_coltypmod; + ListCell *lc_colvalexpr; + int colnum = 0; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "COLUMNS (", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forfour(lc_colname, tf->colnames, + lc_coltype, tf->coltypes, + lc_coltypmod, tf->coltypmods, + lc_colvalexpr, tf->colvalexprs) + { + char *colname = strVal(lfirst(lc_colname)); + JsonExpr *colexpr; + Oid typid; + int32 typmod; + bool ordinality; + JsonBehaviorType default_behavior; + + typid = lfirst_oid(lc_coltype); + typmod = lfirst_int(lc_coltypmod); + colexpr = castNode(JsonExpr, lfirst(lc_colvalexpr)); + + if (colnum < node->colMin) + { + colnum++; + continue; + } + + if (colnum > node->colMax) + break; + + if (colnum > node->colMin) + appendStringInfoString(buf, ", "); + + colnum++; + + ordinality = !colexpr; + + appendContextKeyword(context, "", 0, 0, 0); + + appendStringInfo(buf, "%s %s", quote_identifier(colname), + ordinality ? "FOR ORDINALITY" : + format_type_with_typemod(typid, typmod)); + if (ordinality) + continue; + + if (colexpr->op == JSON_EXISTS_OP) + { + appendStringInfoString(buf, " EXISTS"); + default_behavior = JSON_BEHAVIOR_FALSE; + } + else + { + if (colexpr->op == JSON_QUERY_OP) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(typid, &typcategory, &typispreferred); + + if (typcategory == TYPCATEGORY_STRING) + appendStringInfoString(buf, + colexpr->format->format_type == JS_FORMAT_JSONB ? + " FORMAT JSONB" : " FORMAT JSON"); + } + + default_behavior = JSON_BEHAVIOR_NULL; + } + + if (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR) + default_behavior = JSON_BEHAVIOR_ERROR; + + appendStringInfoString(buf, " PATH "); + + get_json_path_spec(colexpr->path_spec, context, showimplicit); + + get_json_expr_options(colexpr, context, default_behavior); + } + + if (node->child) + get_json_table_nested_columns(tf, node->child, context, showimplicit, + node->colMax >= node->colMin); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_json_table - Parse back a JSON_TABLE function + * ---------- + */ +static void +get_json_table(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + + appendStringInfoString(buf, "JSON_TABLE("); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_const_expr(root->path->value, context, -1); + + appendStringInfo(buf, " AS %s", quote_identifier(root->path->name)); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PASSING ", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr((Node *) lfirst(lc2), context, false); + appendStringInfo(buf, " AS %s", + quote_identifier((lfirst_node(String, lc1))->sval) + ); + } + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + } + + get_json_table_columns(tf, root, context, showimplicit); + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PLAN ", 0, 0, 0); + get_json_table_plan(tf, (Node *) root, context, true); + + if (jexpr->on_error->btype != JSON_BEHAVIOR_EMPTY) + get_json_behavior(jexpr->on_error, context, "ERROR"); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_tablefunc - Parse back a table function + * ---------- + */ +static void +get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + /* XMLTABLE and JSON_TABLE are the only existing implementations. */ + + if (tf->functype == TFT_XMLTABLE) + get_xmltable(tf, context, showimplicit); + else if (tf->functype == TFT_JSON_TABLE) + get_json_table(tf, context, showimplicit); +} + /* ---------- * get_from_clause - Parse back a FROM clause * diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 584bf7001a..91453681e3 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1879,6 +1879,8 @@ typedef struct TableFuncScanState ExprState *rowexpr; /* state for row-generating expression */ List *colexprs; /* state for column-generating expression */ List *coldefexprs; /* state for column default expressions */ + List *colvalexprs; /* state for column value expression */ + List *passingvalexprs; /* state for PASSING argument expression */ List *ns_names; /* same as TableFunc.ns_names */ List *ns_uris; /* list of states of namespace URI exprs */ Bitmapset *notnulls; /* nullability flag for each output column */ diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 5e149b0266..d8466a2699 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -112,6 +112,8 @@ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *expr, JsonFormat *format); extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); +extern Node *makeJsonTableJoinedPlan(JsonTablePlanJoinType type, + Node *plan1, Node *plan2, int location); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 7d63a37d5f..b15f71cc92 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1733,6 +1733,19 @@ typedef enum JsonQuotes */ typedef char *JsonPathSpec; +/* + * JsonTableColumnType - + * enumeration of JSON_TABLE column types + */ +typedef enum JsonTableColumnType +{ + JTC_FOR_ORDINALITY, + JTC_REGULAR, + JTC_EXISTS, + JTC_FORMATTED, + JTC_NESTED, +} JsonTableColumnType; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1786,6 +1799,83 @@ typedef struct JsonFuncExpr int location; /* token location, or -1 if unknown */ } JsonFuncExpr; +/* + * JsonTableColumn - + * untransformed representation of JSON_TABLE column + */ +typedef struct JsonTableColumn +{ + NodeTag type; + JsonTableColumnType coltype; /* column type */ + char *name; /* column name */ + TypeName *typeName; /* column type name */ + char *pathspec; /* path specification, if any */ + char *pathname; /* path name, if any */ + JsonFormat *format; /* JSON format clause, if specified */ + JsonWrapper wrapper; /* WRAPPER behavior for formatted columns */ + bool omit_quotes; /* omit or keep quotes on scalar strings? */ + List *columns; /* nested columns */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + int location; /* token location, or -1 if unknown */ +} JsonTableColumn; + +/* + * JsonTablePlanType - + * flags for JSON_TABLE plan node types representation + */ +typedef enum JsonTablePlanType +{ + JSTP_DEFAULT, + JSTP_SIMPLE, + JSTP_JOINED, +} JsonTablePlanType; + +/* + * JsonTablePlanJoinType - + * flags for JSON_TABLE join types representation + */ +typedef enum JsonTablePlanJoinType +{ + JSTPJ_INNER = 0x01, + JSTPJ_OUTER = 0x02, + JSTPJ_CROSS = 0x04, + JSTPJ_UNION = 0x08, +} JsonTablePlanJoinType; + +typedef struct JsonTablePlan JsonTablePlan; + +/* + * JsonTablePlan - + * untransformed representation of JSON_TABLE plan node + */ +struct JsonTablePlan +{ + NodeTag type; + JsonTablePlanType plan_type; /* plan type */ + JsonTablePlanJoinType join_type; /* join type (for joined plan only) */ + JsonTablePlan *plan1; /* first joined plan */ + JsonTablePlan *plan2; /* second joined plan */ + char *pathname; /* path name (for simple plan only) */ + int location; /* token location, or -1 if unknown */ +}; + +/* + * JsonTable - + * untransformed representation of JSON_TABLE + */ +typedef struct JsonTable +{ + NodeTag type; + JsonCommon *common; /* common JSON path syntax fields */ + List *columns; /* list of JsonTableColumn */ + JsonTablePlan *plan; /* join plan, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + Alias *alias; /* table alias in FROM clause */ + bool lateral; /* does it have LATERAL prefix? */ + int location; /* token location, or -1 if unknown */ +} JsonTable; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 3937114743..cea15cc4e5 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -86,8 +86,14 @@ typedef struct RangeVar int location; } RangeVar; +typedef enum TableFuncType +{ + TFT_XMLTABLE, + TFT_JSON_TABLE +} TableFuncType; + /* - * TableFunc - node for a table function, such as XMLTABLE. + * TableFunc - node for a table function, such as XMLTABLE or JSON_TABLE. * * Entries in the ns_names list are either String nodes containing * literal namespace names, or NULL pointers to represent DEFAULT. @@ -95,6 +101,8 @@ typedef struct RangeVar typedef struct TableFunc { NodeTag type; + /* XMLTABLE or JSON_TABLE */ + TableFuncType functype; /* list of namespace URI expressions */ List *ns_uris pg_node_attr(query_jumble_ignore); /* list of namespace names or NULL */ @@ -115,8 +123,14 @@ typedef struct TableFunc List *colexprs; /* list of column default expressions */ List *coldefexprs pg_node_attr(query_jumble_ignore); + /* list of column value expressions */ + List *colvalexprs pg_node_attr(query_jumble_ignore); + /* list of PASSING argument expressions */ + List *passingvalexprs pg_node_attr(query_jumble_ignore); /* nullability flag for each output column */ Bitmapset *notnulls pg_node_attr(query_jumble_ignore); + /* JSON_TABLE plan */ + Node *plan pg_node_attr(query_jumble_ignore); /* counts from 0; -1 if none specified */ int ordinalitycol pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ @@ -1552,7 +1566,8 @@ typedef enum JsonExprOp { JSON_VALUE_OP, /* JSON_VALUE() */ JSON_QUERY_OP, /* JSON_QUERY() */ - JSON_EXISTS_OP /* JSON_EXISTS() */ + JSON_EXISTS_OP, /* JSON_EXISTS() */ + JSON_TABLE_OP /* JSON_TABLE() */ } JsonExprOp; /* @@ -1770,6 +1785,48 @@ typedef struct JsonExpr int location; /* token location, or -1 if unknown */ } JsonExpr; +/* + * JsonTablePath + * A JSON path expression to be computed as part of evaluating + * a JSON_TABLE plan node + */ +typedef struct JsonTablePath +{ + NodeTag type; + + Const *value; + char *name; +} JsonTablePath; + +/* + * JsonTableParent - + * transformed representation of parent JSON_TABLE plan node + */ +typedef struct JsonTableParent +{ + NodeTag type; + JsonTablePath *path; + Node *child; /* nested columns, if any */ + bool outerJoin; /* outer or inner join for nested columns? */ + int colMin; /* min column index in the resulting column + * list */ + int colMax; /* max column index in the resulting column + * list */ + bool errorOnError; /* ERROR/EMPTY ON ERROR behavior */ +} JsonTableParent; + +/* + * JsonTableSibling - + * transformed representation of joined sibling JSON_TABLE plan node + */ +typedef struct JsonTableSibling +{ + NodeTag type; + Node *larg; /* left join node */ + Node *rarg; /* right join node */ + bool cross; /* cross or union join? */ +} JsonTableSibling; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0954d9fc7b..f88a6c9ac6 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -242,6 +242,7 @@ PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_table", JSON_TABLE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) @@ -284,6 +285,7 @@ PG_KEYWORD("names", NAMES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("national", NATIONAL, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("natural", NATURAL, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("nchar", NCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("nested", NESTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("new", NEW, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("next", NEXT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("nfc", NFC, UNRESERVED_KEYWORD, BARE_LABEL) @@ -334,7 +336,9 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h index 8e26482db4..7152a8ef48 100644 --- a/src/include/parser/parse_clause.h +++ b/src/include/parser/parse_clause.h @@ -51,4 +51,7 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle, extern Index assignSortGroupRef(TargetEntry *tle, List *tlist); extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList); +/* functions in parse_jsontable.c */ +extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt); + #endif /* PARSE_CLAUSE_H */ diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index 4dae78a98c..b7ada77cb1 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -15,6 +15,7 @@ #define JSONPATH_H #include "fmgr.h" +#include "executor/tablefunc.h" #include "nodes/pg_list.h" #include "nodes/primnodes.h" #include "utils/jsonb.h" @@ -288,4 +289,6 @@ extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars); +extern PGDLLIMPORT const TableFuncRoutine JsonbTableRoutine; + #endif diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out index 8b87580752..995f267404 100644 --- a/src/test/regress/expected/json_sqljson.out +++ b/src/test/regress/expected/json_sqljson.out @@ -16,3 +16,9 @@ ERROR: JSON_QUERY() is not yet implemented for the json type LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); ^ HINT: Try casting the argument to jsonb +-- JSON_TABLE +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); +ERROR: JSON_TABLE() is not yet implemented for the json type +LINE 1: SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo ... + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out index 22f8679917..4323ed6b35 100644 --- a/src/test/regress/expected/jsonb_sqljson.out +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -1040,3 +1040,1072 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.dateti ERROR: functions in index expression must be marked IMMUTABLE CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; +-- JSON_TABLE +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); +ERROR: syntax error at or near "(" +LINE 1: SELECT JSON_TABLE('[]', '$'); + ^ +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); +ERROR: syntax error at or near ")" +LINE 1: SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + ^ +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); +ERROR: JSON_TABLE function has 1 columns available but 2 columns specified +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + foo +----- +(0 rows) + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + item | foo +------+----- + 123 | +(1 row) + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + js | id | id2 | int | text | char(4) | bool | numeric | domain | js | jb | jst | jsc | jsv | jsb | jsbq | aaa | aaa1 | exists1 | exists2 | exists3 | exists4 | js2 | jsb2w | jsb2q | ia | ta | jba +---------------------------------------------------------------------------------------+----+-----+-----+---------+---------+------+---------+---------+--------------+--------------+--------------+------+------+--------------+--------------+-----+------+---------+---------+---------+---------+--------------+----------------+--------------+----+----+----- + 1 | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [] | | | | | | | | | | | | | | | | | | | | | | | | | | | + {} | 1 | 1 | | | | | | | {} | {} | {} | {} | {} | {} | {} | | | f | 0 | | false | {} | [{}] | {} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 2 | 2 | 1 | 1.23 | 1.23 | | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | | | f | 0 | | false | 1.23 | [1.23] | 1.23 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 3 | 3 | 2 | 2 | 2 | | 2 | 2 | "2" | "2" | "2" | "2" | "2" | "2" | 2 | | | f | 0 | | false | "2" | ["2"] | 2 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 4 | 4 | | aaaaaaa | aaaa | | | aaaaaaa | "aaaaaaa" | "aaaaaaa" | "aaaaaaa" | "aaa | "aaa | "aaaaaaa" | | | | f | 0 | | false | "aaaaaaa" | ["aaaaaaa"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 5 | 5 | | foo | foo | | | | "foo" | "foo" | "foo" | "foo | "foo | "foo" | | | | f | 0 | | false | "foo" | ["foo"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 6 | 6 | | | | | | | null | null | null | null | null | null | null | | | f | 0 | | false | null | [null] | null | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 7 | 7 | 0 | false | fals | f | | false | false | false | false | fals | fals | false | false | | | f | 0 | | false | false | [false] | false | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 8 | 8 | 1 | true | true | t | | true | true | true | true | true | true | true | true | | | f | 0 | | false | true | [true] | true | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 9 | 9 | | | | | | | {"aaa": 123} | {"aaa": 123} | {"aaa": 123} | {"aa | {"aa | {"aaa": 123} | {"aaa": 123} | 123 | 123 | t | 1 | 1 | true | {"aaa": 123} | [{"aaa": 123}] | {"aaa": 123} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 10 | 10 | | [1,2] | [1,2 | | | [1,2] | "[1,2]" | "[1,2]" | "[1,2]" | "[1, | "[1, | "[1,2]" | [1, 2] | | | f | 0 | | false | "[1,2]" | ["[1,2]"] | [1, 2] | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 11 | 11 | | "str" | "str | | | "str" | "\"str\"" | "\"str\"" | "\"str\"" | "\"s | "\"s | "\"str\"" | "str" | | | f | 0 | | false | "\"str\"" | ["\"str\""] | "str" | | | +(14 rows) + +-- JSON_TABLE: Test backward parsing +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); +\sv jsonb_table_view +CREATE OR REPLACE VIEW public.jsonb_table_view AS + SELECT id, + id2, + "int", + text, + "char(4)", + bool, + "numeric", + domain, + js, + jb, + jst, + jsc, + jsv, + jsb, + jsbq, + aaa, + aaa1, + exists1, + exists2, + exists3, + js2, + jsb2w, + jsb2q, + ia, + ta, + jba, + a1, + b1, + a11, + a21, + a22 + FROM JSON_TABLE( + 'null'::jsonb, '$[*]' AS json_table_path_1 + PASSING + 1 + 2 AS a, + '"foo"'::json AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, + "int" integer PATH '$', + text text PATH '$', + "char(4)" character(4) PATH '$', + bool boolean PATH '$', + "numeric" numeric PATH '$', + domain jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc character(4) FORMAT JSON PATH '$', + jsv character varying(4) FORMAT JSON PATH '$', + jsb jsonb PATH '$', + jsbq jsonb PATH '$' OMIT QUOTES, + aaa integer PATH '$."aaa"', + aaa1 integer PATH '$."aaa"', + exists1 boolean EXISTS PATH '$."aaa"', + exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia integer[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 + COLUMNS ( + a1 integer PATH '$."a1"', + b1 text PATH '$."b1"', + NESTED PATH '$[*]' AS "p1 1" + COLUMNS ( + a11 text PATH '$."a11"' + ) + ), + NESTED PATH '$[2]' AS p2 + COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" + COLUMNS ( + a21 text PATH '$."a21"' + ), + NESTED PATH '$[*]' AS p22 + COLUMNS ( + a22 text PATH '$."a22"' + ) + ) + ) + PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22)))) + ) +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Table Function Scan on "json_table" + Output: "json_table".id, "json_table".id2, "json_table"."int", "json_table".text, "json_table"."char(4)", "json_table".bool, "json_table"."numeric", "json_table".domain, "json_table".js, "json_table".jb, "json_table".jst, "json_table".jsc, "json_table".jsv, "json_table".jsb, "json_table".jsbq, "json_table".aaa, "json_table".aaa1, "json_table".exists1, "json_table".exists2, "json_table".exists3, "json_table".js2, "json_table".jsb2w, "json_table".jsb2q, "json_table".ia, "json_table".ta, "json_table".jba, "json_table".a1, "json_table".b1, "json_table".a11, "json_table".a21, "json_table".a22 + Table Function Call: JSON_TABLE('null'::jsonb, '$[*]' AS json_table_path_1 PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (id FOR ORDINALITY, id2 FOR ORDINALITY, "int" integer PATH '$', text text PATH '$', "char(4)" character(4) PATH '$', bool boolean PATH '$', "numeric" numeric PATH '$', domain jsonb_test_domain PATH '$', js json PATH '$', jb jsonb PATH '$', jst text FORMAT JSON PATH '$', jsc character(4) FORMAT JSON PATH '$', jsv character varying(4) FORMAT JSON PATH '$', jsb jsonb PATH '$', jsbq jsonb PATH '$' OMIT QUOTES, aaa integer PATH '$."aaa"', aaa1 integer PATH '$."aaa"', exists1 boolean EXISTS PATH '$."aaa"', exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, js2 json PATH '$', jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, jsb2q jsonb PATH '$' OMIT QUOTES, ia integer[] PATH '$', ta text[] PATH '$', jba jsonb[] PATH '$', NESTED PATH '$[1]' AS p1 COLUMNS (a1 integer PATH '$."a1"', b1 text PATH '$."b1"', NESTED PATH '$[*]' AS "p1 1" COLUMNS (a11 text PATH '$."a11"')), NESTED PATH '$[2]' AS p2 COLUMNS ( NESTED PATH '$[*]' AS "p2:1" COLUMNS (a21 text PATH '$."a21"'), NESTED PATH '$[*]' AS p22 COLUMNS (a22 text PATH '$."a22"'))) PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22))))) +(3 rows) + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + js | a +-------+--- + 1 | 1 + "err" | +(2 rows) + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; + a +--- + +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: jsonpath member accessor can only be applied to an object +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: no SQL/JSON item +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 1 +(1 row) + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); + a +--- + 0 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to smallint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to bigint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to real +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 E... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); + a +----- + fal +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to json +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to jsonb +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EX... + ^ +-- JSON_TABLE: nested paths and plans +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb '[]', '$' -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 4: NESTED PATH '$' COLUMNS ( -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: b +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +-- JSON_TABLE: plan validation +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p1) + ^ +DETAIL: Path name mismatch: expected p0 but p1 is given. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 UNION p1 UNION p11) + ^ +DETAIL: Expected INNER or OUTER JSON_TABLE plan node. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 8: NESTED PATH '$' AS p2 COLUMNS ( + ^ +DETAIL: Plan node for nested path p2 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 5: NESTED PATH '$' AS p11 COLUMNS ( foo int ), + ^ +DETAIL: Plan node for nested path p11 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) + ^ +DETAIL: Plan node contains some extra or duplicate sibling nodes. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 6: NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ^ +DETAIL: Plan node for nested path p12 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 9: NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ^ +DETAIL: Plan node for nested path p21 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + bar | foo | baz +-----+-----+----- +(0 rows) + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb 'null', 'strict $[*]' -- without root path name + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- JSON_TABLE: plan execution +CREATE TEMP TABLE jsonb_table_test (js jsonb); +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + n | a | c | b +---+----+----+--- + 1 | 1 | | + 2 | 2 | 10 | + 2 | 2 | | + 2 | 2 | 20 | + 2 | 2 | | 1 + 2 | 2 | | 2 + 2 | 2 | | 3 + 3 | 3 | | 1 + 3 | 3 | | 2 + 4 | -1 | | 1 + 4 | -1 | | 2 +(11 rows) + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + n | a | b | b1 | c | c1 | b +---+---+--------------+-----+------+----+----- + 1 | 1 | [1, 10] | 1 | 1 | | 101 + 1 | 1 | [1, 10] | 1 | null | | 101 + 1 | 1 | [1, 10] | 1 | 2 | | 101 + 1 | 1 | [1, 10] | 10 | 1 | | 110 + 1 | 1 | [1, 10] | 10 | null | | 110 + 1 | 1 | [1, 10] | 10 | 2 | | 110 + 1 | 1 | [2] | 2 | 1 | | 102 + 1 | 1 | [2] | 2 | null | | 102 + 1 | 1 | [2] | 2 | 2 | | 102 + 1 | 1 | [3, 30, 300] | 3 | 1 | | 103 + 1 | 1 | [3, 30, 300] | 3 | null | | 103 + 1 | 1 | [3, 30, 300] | 3 | 2 | | 103 + 1 | 1 | [3, 30, 300] | 30 | 1 | | 130 + 1 | 1 | [3, 30, 300] | 30 | null | | 130 + 1 | 1 | [3, 30, 300] | 30 | 2 | | 130 + 1 | 1 | [3, 30, 300] | 300 | 1 | | 400 + 1 | 1 | [3, 30, 300] | 300 | null | | 400 + 1 | 1 | [3, 30, 300] | 300 | 2 | | 400 + 2 | 2 | | | | | + 3 | | | | | | +(20 rows) + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + x | y | y | z +---+---+--------------+--- + 2 | 1 | [1, 2, 3] | 1 + 2 | 1 | [1, 2, 3] | 2 + 2 | 1 | [1, 2, 3] | 3 + 3 | 1 | [1, 2, 3] | 1 + 3 | 1 | [1, 2, 3] | 2 + 3 | 1 | [1, 2, 3] | 3 + 3 | 1 | [2, 3, 4, 5] | 2 + 3 | 1 | [2, 3, 4, 5] | 3 + 3 | 1 | [2, 3, 4, 5] | 4 + 3 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [1, 2, 3] | 1 + 4 | 1 | [1, 2, 3] | 2 + 4 | 1 | [1, 2, 3] | 3 + 4 | 1 | [2, 3, 4, 5] | 2 + 4 | 1 | [2, 3, 4, 5] | 3 + 4 | 1 | [2, 3, 4, 5] | 4 + 4 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [3, 4, 5, 6] | 3 + 4 | 1 | [3, 4, 5, 6] | 4 + 4 | 1 | [3, 4, 5, 6] | 5 + 4 | 1 | [3, 4, 5, 6] | 6 + 2 | 2 | [1, 2, 3] | 2 + 2 | 2 | [1, 2, 3] | 3 + 3 | 2 | [1, 2, 3] | 2 + 3 | 2 | [1, 2, 3] | 3 + 3 | 2 | [2, 3, 4, 5] | 2 + 3 | 2 | [2, 3, 4, 5] | 3 + 3 | 2 | [2, 3, 4, 5] | 4 + 3 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [1, 2, 3] | 2 + 4 | 2 | [1, 2, 3] | 3 + 4 | 2 | [2, 3, 4, 5] | 2 + 4 | 2 | [2, 3, 4, 5] | 3 + 4 | 2 | [2, 3, 4, 5] | 4 + 4 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [3, 4, 5, 6] | 3 + 4 | 2 | [3, 4, 5, 6] | 4 + 4 | 2 | [3, 4, 5, 6] | 5 + 4 | 2 | [3, 4, 5, 6] | 6 + 2 | 3 | [1, 2, 3] | 3 + 3 | 3 | [1, 2, 3] | 3 + 3 | 3 | [2, 3, 4, 5] | 3 + 3 | 3 | [2, 3, 4, 5] | 4 + 3 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [1, 2, 3] | 3 + 4 | 3 | [2, 3, 4, 5] | 3 + 4 | 3 | [2, 3, 4, 5] | 4 + 4 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [3, 4, 5, 6] | 3 + 4 | 3 | [3, 4, 5, 6] | 4 + 4 | 3 | [3, 4, 5, 6] | 5 + 4 | 3 | [3, 4, 5, 6] | 6 +(52 rows) + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; +ERROR: could not find jsonpath variable "x" +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); + json_value +------------ + foo +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_query +------------ + 123 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); + json_query +------------ + [123] +(1 row) + +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +ERROR: syntax error at or near " " of jsonpath input +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); +ERROR: only string constants supported in JSON_TABLE path specification +LINE 1: SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || '... + ^ diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql index 4f30fa46b9..df4a430d88 100644 --- a/src/test/regress/sql/json_sqljson.sql +++ b/src/test/regress/sql/json_sqljson.sql @@ -9,3 +9,7 @@ SELECT JSON_VALUE(NULL FORMAT JSON, '$'); -- JSON_QUERY SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + +-- JSON_TABLE + +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql index b8a6148b7b..fbd86f2084 100644 --- a/src/test/regress/sql/jsonb_sqljson.sql +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -325,3 +325,632 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; + +-- JSON_TABLE + +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); + +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); + +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); + +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + +-- JSON_TABLE: Test backward parsing + +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); + +\sv jsonb_table_view + +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; + +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); + +-- JSON_TABLE: nested paths and plans + +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; + +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; + +-- JSON_TABLE: plan validation + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; + +-- JSON_TABLE: plan execution + +CREATE TEMP TABLE jsonb_table_test (js jsonb); + +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); + +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; + +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 6e4c026492..642a29d8d5 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1303,6 +1303,7 @@ JsonPathKeyword JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback +JsonPathSpec JsonPathString JsonPathVarCallback JsonPathVariable @@ -1311,6 +1312,17 @@ JsonQuotes JsonReturning JsonSemAction JsonSerializeExpr +JsonTable +JsonTableColumn +JsonTableColumnType +JsonTableContext +JsonTableJoinState +JsonTableParent +JsonTablePlan +JsonTablePlanJoinType +JsonTablePlanType +JsonTableScanState +JsonTableSibling JsonTokenType JsonTransformStringValuesAction JsonTypeCategory @@ -2765,6 +2777,7 @@ TableFunc TableFuncRoutine TableFuncScan TableFuncScanState +TableFuncType TableInfo TableLikeClause TableSampleClause -- 2.35.3
v5-0005-SQL-JSON-query-functions.patch (application/octet-stream, 208.5K) download From cbbcd78d0b7d992f8353d8279ec6e0fbf4c8d3ba Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Fri, 16 Jun 2023 17:49:18 +0900 Subject: [PATCH v5 5/7] SQL/JSON query functions This introduces the SQL/JSON functions for querying JSON data using jsonpath expressions. The functions are: JSON_EXISTS() JSON_QUERY() JSON_VALUE() All of these functions only operate on jsonb. The workaround for now is to cast the argument to jsonb. JSON_EXISTS() tests if the jsonpath expression applied to the jsonb value yields any values. JSON_VALUE() must return a single value, and an error occurs if it tries to return multiple values. JSON_QUERY() must return a json object or array, and there are various WRAPPER options for handling scalar or multi-value results. Both these functions have options for handling EMPTY and ERROR conditions. Nikita Glukhov Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby. Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org --- doc/src/sgml/func.sgml | 147 +++ src/backend/executor/execExpr.c | 432 ++++++++ src/backend/executor/execExprInterp.c | 502 ++++++++- src/backend/jit/llvm/llvmjit_expr.c | 250 +++++ src/backend/jit/llvm/llvmjit_types.c | 5 + src/backend/nodes/makefuncs.c | 15 + src/backend/nodes/nodeFuncs.c | 183 ++++ src/backend/optimizer/path/costsize.c | 3 +- src/backend/optimizer/util/clauses.c | 19 + src/backend/parser/gram.y | 348 ++++++- src/backend/parser/parse_collate.c | 7 + src/backend/parser/parse_expr.c | 519 ++++++++- src/backend/parser/parse_target.c | 15 + src/backend/utils/adt/formatting.c | 44 + src/backend/utils/adt/jsonb.c | 62 ++ src/backend/utils/adt/jsonfuncs.c | 170 ++- src/backend/utils/adt/jsonpath.c | 255 +++++ src/backend/utils/adt/jsonpath_exec.c | 391 ++++++- src/backend/utils/adt/ruleutils.c | 136 +++ src/include/executor/execExpr.h | 172 +++ src/include/executor/executor.h | 1 + src/include/nodes/execnodes.h | 3 + src/include/nodes/makefuncs.h | 1 + src/include/nodes/parsenodes.h | 48 + src/include/nodes/primnodes.h | 109 ++ src/include/parser/kwlist.h | 11 + src/include/utils/formatting.h | 2 +- src/include/utils/jsonb.h | 3 + src/include/utils/jsonfuncs.h | 5 + src/include/utils/jsonpath.h | 27 + src/interfaces/ecpg/preproc/ecpg.trailer | 28 + src/test/regress/expected/json_sqljson.out | 18 + src/test/regress/expected/jsonb_sqljson.out | 1042 +++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/json_sqljson.sql | 11 + src/test/regress/sql/jsonb_sqljson.sql | 327 ++++++ src/tools/pgindent/typedefs.list | 14 + 37 files changed, 5234 insertions(+), 93 deletions(-) create mode 100644 src/test/regress/expected/json_sqljson.out create mode 100644 src/test/regress/expected/jsonb_sqljson.out create mode 100644 src/test/regress/sql/json_sqljson.sql create mode 100644 src/test/regress/sql/jsonb_sqljson.sql diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 9cece06c18..cb36e1463b 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16995,6 +16995,153 @@ array w/o UK? | t </tbody> </tgroup> </table> + + <para> + <xref linkend="functions-sqljson-querying"/> details the SQL/JSON + functions that can be used to query JSON data. + </para> + + <note> + <para> + SQL/JSON paths can only be applied to the <type>jsonb</type> type, so it + might be necessary to cast the <replaceable>context_item</replaceable> + argument of these functions to <type>jsonb</type>. + </para> + </note> + + <table id="functions-sqljson-querying"> + <title>SQL/JSON Query Functions</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="func_table_entry"><para role="func_signature"> + Function signature + </para> + <para> + Description + </para> + <para> + Example(s) + </para></entry> + </row> + </thead> + <tbody> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_exists</primary></indexterm> + <function>json_exists</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>TRUE</literal> | <literal>FALSE</literal> |<literal> UNKNOWN</literal> | <literal>ERROR</literal> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns true if the SQL/JSON <replaceable>path_expression</replaceable> + applied to the <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s yields any items. + The <literal>ON ERROR</literal> clause specifies what is returned if + an error occurs. Note that if the <replaceable>path_expression</replaceable> + is <literal>strict</literal>, an error is generated if it yields no items. + The default value is <literal>UNKNOWN</literal> which causes a NULL + result. + </para> + <para> + <literal>json_exists(jsonb '{"key1": [1,2,3]}', 'strict $.key1[*] ? (@ > 2)')</literal> + <returnvalue>t</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'lax $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>f</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'strict $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>ERROR: jsonpath array subscript is out of bounds</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_value</primary></indexterm> + <function>json_value</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> + <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. The extracted value must be + a single <acronym>SQL/JSON</acronym> scalar item. For results that + are objects or arrays, use the <function>json_query</function> + function instead. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + The <literal>ON EMPTY</literal> clause specifies the behavior if the + <replaceable>path_expression</replaceable> yields no value at all. + The <literal>ON ERROR</literal> clause specifies the behavior if an + error occurs as a result of <type>jsonpath</type> evaluation + (including cast to the output type) or execution of + <literal>ON EMPTY</literal> behavior (that was caused by empty result + of <type>jsonpath</type> evaluation). + </para> + <para> + <literal>json_value(jsonb '"123.45"', '$' RETURNING float)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_value(jsonb '"03:04 2015-02-01"', '$.datetime("HH24:MI&nbsp;YYYY-MM-DD")' RETURNING date)</literal> + <returnvalue>2015-02-01</returnvalue> + </para> + <para> + <literal>json_value(jsonb '[1,2]', 'strict $[*]' DEFAULT 9 ON ERROR)</literal> + <returnvalue>9</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_query</primary></indexterm> + <function>json_query</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional> + <optional> { <literal>WITHOUT</literal> | <literal>WITH</literal> { <literal>CONDITIONAL</literal> | <optional><literal>UNCONDITIONAL</literal></optional> } } <optional> <literal>ARRAY</literal> </optional> <literal>WRAPPER</literal> </optional> + <optional> { <literal>KEEP</literal> | <literal>OMIT</literal> } <literal>QUOTES</literal> <optional> <literal>ON SCALAR STRING</literal> </optional> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. + This function must return a JSON string, so if the path expression + returns multiple SQL/JSON items, you must wrap the result using the + <literal>WITH WRAPPER</literal> clause. If the wrapper is + <literal>UNCONDITIONAL</literal>, an array wrapper will always + be applied, even if the returned value is already a single JSON object + or array, but if it is <literal>CONDITIONAL</literal>, it will not be + applied to a single array or object. <literal>UNCONDITIONAL</literal> + is the default. + If the result is a scalar string, by default the value returned will have + surrounding quotes making it a valid JSON value. However, this behavior + is reversed if <literal>OMIT QUOTES</literal> is specified. + The <literal>ON ERROR</literal> and <literal>ON EMPTY</literal> + clauses have similar semantics to those clauses for + <function>json_value</function>. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + </para> + <para> + <literal>json_query(jsonb '[1,[2,3],null]', 'lax $[*][1]' WITH CONDITIONAL WRAPPER)</literal> + <returnvalue>[3]</returnvalue> + </para></entry> + </row> + </tbody> + </tgroup> + </table> + </sect2> <sect2 id="functions-sqljson-path"> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 6ca4098bef..d3d2ce00d1 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -49,6 +49,7 @@ #include "utils/builtins.h" #include "utils/datum.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -88,6 +89,16 @@ static void ExecBuildAggTransCall(ExprState *state, AggState *aggstate, FunctionCallInfo fcinfo, AggStatePerTrans pertrans, int transno, int setno, int setoff, bool ishash, bool nullcheck); +static void ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch); +static Datum GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null); +static JsonCoercionState *ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull); +static JsonItemCoercionsState *ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull); /* @@ -2411,6 +2422,14 @@ ExecInitExprRec(Expr *node, ExprState *state, break; } + case T_JsonExpr: + { + JsonExpr *jexpr = castNode(JsonExpr, node); + + ExecInitJsonExpr(jexpr, state, resv, resnull, &scratch); + break; + } + case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -4178,3 +4197,416 @@ ExecBuildParamSetEqual(TupleDesc desc, return state; } + +/* + * Push steps to evaluate a JsonExpr and its various subsidiary expressions. + */ +static void +ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch) +{ + JsonExprState *jsestate = palloc0(sizeof(JsonExprState)); + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + ListCell *argexprlc; + ListCell *argnamelc; + int skip_step_off = -1; + int passing_args_step_off = -1; + int coercion_step_off = -1; + int coercion_finish_step_off = -1; + int behavior_step_off = -1; + int onempty_expr_step_off = -1; + int onempty_jump_step_off = -1; + int onerror_expr_step_off = -1; + int onerror_jump_step_off = -1; + List *adjust_jumps = NIL; + ListCell *lc; + ExprEvalStep *as; + + jsestate->jsexpr = jexpr; + + /* + * Add steps to compute formatted_expr, pathspec, and PASSING arg + * expressions as things that must be evaluated *before* the actual JSON + * path expression. + */ + ExecInitExprRec((Expr *) jexpr->formatted_expr, state, + &pre_eval->formatted_expr.value, + &pre_eval->formatted_expr.isnull); + ExecInitExprRec((Expr *) jexpr->path_spec, state, + &pre_eval->pathspec.value, + &pre_eval->pathspec.isnull); + + /* + * Before pushing steps for PASSING args, push a step to decide whether to + * skip evaluating the args and the JSON path expression depending on + * whether either of formatted_expr and pathspec is NULL; see + * ExecEvalJsonExprSkip(). + */ + scratch->opcode = EEOP_JSONEXPR_SKIP; + scratch->d.jsonexpr_skip.jsestate = jsestate; + skip_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* PASSING args. */ + jsestate->pre_eval.args = NIL; + passing_args_step_off = state->steps_len; + forboth(argexprlc, jexpr->passing_values, + argnamelc, jexpr->passing_names) + { + Expr *argexpr = (Expr *) lfirst(argexprlc); + String *argname = lfirst_node(String, argnamelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(argname->sval); + var->typid = exprType((Node *) argexpr); + var->typmod = exprTypmod((Node *) argexpr); + + ExecInitExprRec((Expr *) argexpr, state, &var->value, &var->isnull); + + pre_eval->args = lappend(pre_eval->args, var); + } + + /* + * Step for the actual JSON path evaluation; see ExecEvalJson() and + * ExecEvalJsonExpr(). + */ + scratch->opcode = EEOP_JSONEXPR_PATH; + scratch->d.jsonexpr.jsestate = jsestate; + ExprEvalPushStep(state, scratch); + + /* + * Push steps to control the evaluation of expressions based on the result + * of JSON path evaluation. + */ + + /* + * Step to handle ON ERROR and ON EMPTY behavior; see + * ExecEvalJsonExprBehavior(). + */ + scratch->opcode = EEOP_JSONEXPR_BEHAVIOR; + scratch->d.jsonexpr_behavior.jsestate = jsestate; + behavior_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Step(s) to evaluate ON EMPTY expression */ + onempty_expr_step_off = state->steps_len; + if (jexpr->on_empty && + jexpr->on_empty->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_empty->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_empty->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_empty, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onempty_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + } + } + + /* Step(s) to evaluate ON ERROR expression */ + onerror_expr_step_off = state->steps_len; + if (jexpr->on_error && + jexpr->on_error->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_error->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_error->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_error, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onerror_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set later */ + ExprEvalPushStep(state, scratch); + } + } + + /* + * Step to handle applying coercion to the JSON item returned by + * EEOP_JSONEXPR_PATH or to the ON EMPTY/ERROR expression as + * EEOP_JSONEXPR_BEHAVIOR decides; see ExecEvalJsonExprCoercion(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Initialize coercion expression(s). */ + if (jexpr->result_coercion) + { + jsestate->result_jcstate = + ExecInitJsonCoercion(scratch, state, jexpr->result_coercion, + resv, resnull); + /* See the comment above. */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* computed later */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + if (jexpr->coercions) + { + /* + * ExecPrepareJsonItemCoercion() called by ExecEvalJsonExpr() chooses + * one for a given JSON item returned by JsonPathValue(). + */ + jsestate->item_jcstates = + ExecInitJsonItemCoercions(scratch, state, jexpr->coercions, + resv, resnull); + } + + /* + * And a step to clean up after the coercion step; see + * ExecEvalJsonExprCoercionFinish(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION_FINISH; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_finish_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* + * Adjust jump target addresses in various post-eval steps now that we + * have all the steps in place. + */ + + /* EEOP_JSONEXPR_SKIP */ + Assert(skip_step_off >= 0); + as = &state->steps[skip_step_off]; + as->d.jsonexpr_skip.jump_coercion = coercion_step_off; + as->d.jsonexpr_skip.jump_passing_args = passing_args_step_off; + + /* EEOP_JSONEXPR_BEHAVIOR */ + Assert(behavior_step_off >= 0); + as = &state->steps[behavior_step_off]; + as->d.jsonexpr_behavior.jump_onerror_expr = onerror_expr_step_off; + as->d.jsonexpr_behavior.jump_onempty_expr = onempty_expr_step_off; + as->d.jsonexpr_behavior.jump_coercion = coercion_step_off; + as->d.jsonexpr_behavior.jump_skip_coercion = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION */ + Assert(coercion_step_off >= 0); + as = &state->steps[coercion_step_off]; + as->d.jsonexpr_coercion.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION_FINISH */ + Assert(coercion_finish_step_off >= 0); + as = &state->steps[coercion_finish_step_off]; + as->d.jsonexpr_coercion_finish.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion_finish.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JUMP steps */ + + /* + * Ones after ON EMPTY and ON ERROR non-default expressions should jump to + * the coercion step. + */ + if (onempty_jump_step_off >= 0) + { + as = &state->steps[onempty_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + if (onerror_jump_step_off >= 0) + { + as = &state->steps[onerror_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + + /* The rest should jump to the end. */ + foreach(lc, adjust_jumps) + { + as = &state->steps[lfirst_int(lc)]; + as->d.jump.jumpdone = state->steps_len; + } + + /* + * Set RETURNING type's input function used by ExecEvalJsonExprCoercion(). + */ + if (jexpr->omit_quotes || + (jexpr->result_coercion && jexpr->result_coercion->via_io)) + { + Oid typinput; + FmgrInfo *finfo; + + /* lookup the result type's input function */ + getTypeInputInfo(jexpr->returning->typid, &typinput, + &jsestate->input.typioparam); + finfo = palloc0(sizeof(FmgrInfo)); + fmgr_info(typinput, finfo); + jsestate->input.finfo = finfo; + } +} + +/* + * Evaluate a JSON error/empty behavior result. + */ +static Datum +GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null) +{ + *is_null = false; + + switch (behavior->btype) + { + case JSON_BEHAVIOR_EMPTY_ARRAY: + return JsonbPGetDatum(JsonbMakeEmptyArray()); + + case JSON_BEHAVIOR_EMPTY_OBJECT: + return JsonbPGetDatum(JsonbMakeEmptyObject()); + + case JSON_BEHAVIOR_TRUE: + return BoolGetDatum(true); + + case JSON_BEHAVIOR_FALSE: + return BoolGetDatum(false); + + case JSON_BEHAVIOR_NULL: + case JSON_BEHAVIOR_UNKNOWN: + case JSON_BEHAVIOR_EMPTY: + *is_null = true; + return (Datum) 0; + + case JSON_BEHAVIOR_DEFAULT: + /* Always handled in the caller. */ + Assert(false); + return (Datum) 0; + + default: + elog(ERROR, "unrecognized SQL/JSON behavior %d", behavior->btype); + return (Datum) 0; + } +} + +/* Initialize one JsonCoercion for execution. */ +static JsonCoercionState * +ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull) +{ + JsonCoercionState *jcstate = palloc0(sizeof(JsonCoercionState)); + + jcstate->coercion = coercion; + if (coercion && coercion->expr) + { + Datum *save_innermost_caseval; + bool *save_innermost_casenull; + + jcstate->jump_eval_expr = state->steps_len; + + /* Push step(s) to compute cstate->coercion. */ + save_innermost_caseval = state->innermost_caseval; + save_innermost_casenull = state->innermost_casenull; + + state->innermost_caseval = resv; + state->innermost_casenull = resnull; + + ExecInitExprRec((Expr *) coercion->expr, state, resv, resnull); + + state->innermost_caseval = save_innermost_caseval; + state->innermost_casenull = save_innermost_casenull; + } + else + jcstate->jump_eval_expr = -1; + + return jcstate; +} + +/* + * Push steps to evaluate coercions from a given JsonItemCoercions, which + * contains all possible coercions that may need to be applied to JSON + * items coming from evaluating the parent JsonExpr. + */ +static JsonItemCoercionsState * +ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull) +{ + JsonCoercion **coercion; + JsonItemCoercionsState *item_jcstates = palloc0(sizeof(JsonItemCoercionsState)); + JsonCoercionState **item_jcstate; + ExprEvalStep *as; + List *adjust_jumps = NIL; + ListCell *lc; + + /* Push the steps of individual coercions. */ + for (coercion = &coercions->null, + item_jcstate = &item_jcstates->null; + coercion <= &coercions->composite; + coercion++, item_jcstate++) + { + *item_jcstate = ExecInitJsonCoercion(scratch, state, *coercion, + resv, resnull); + + /* Emit JUMP step to skip past other coercions' steps. */ + scratch->opcode = EEOP_JUMP; + + /* + * Remember JUMP step address to set the actual jump target address + * below. + */ + adjust_jumps = lappend_int(adjust_jumps, state->steps_len); + ExprEvalPushStep(state, scratch); + } + + foreach(lc, adjust_jumps) + { + int jump_step_id = lfirst_int(lc); + + as = &state->steps[jump_step_id]; + as->d.jump.jumpdone = state->steps_len; + } + + return item_jcstates; +} diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 76e59691e5..4c97e714ea 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -74,6 +74,7 @@ #include "utils/json.h" #include "utils/jsonb.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/timestamp.h" @@ -152,6 +153,9 @@ static TupleDesc get_cached_rowtype(Oid type_id, int32 typmod, bool *changed); static void ExecEvalRowNullInt(ExprState *state, ExprEvalStep *op, ExprContext *econtext, bool checkisnull); +static Datum ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate); /* fast-path evaluation functions */ static Datum ExecJustInnerVar(ExprState *state, ExprContext *econtext, bool *isnull); @@ -480,6 +484,11 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_XMLEXPR, &&CASE_EEOP_JSON_CONSTRUCTOR, &&CASE_EEOP_IS_JSON, + &&CASE_EEOP_JSONEXPR_SKIP, + &&CASE_EEOP_JSONEXPR_PATH, + &&CASE_EEOP_JSONEXPR_BEHAVIOR, + &&CASE_EEOP_JSONEXPR_COERCION, + &&CASE_EEOP_JSONEXPR_COERCION_FINISH, &&CASE_EEOP_AGGREF, &&CASE_EEOP_GROUPING_FUNC, &&CASE_EEOP_WINDOW_FUNC, @@ -1186,8 +1195,14 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) /* second and third arguments are already set up */ fcinfo_in->isnull = false; + + /* Pass down soft-error capture node if any. */ + fcinfo_in->context = state->escontext; + d = FunctionCallInvoke(fcinfo_in); *op->resvalue = d; + if (SOFT_ERROR_OCCURRED(state->escontext)) + *op->resnull = true; /* Should get null result if and only if str is NULL */ if (str == NULL) @@ -1195,7 +1210,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) Assert(*op->resnull); Assert(fcinfo_in->isnull); } - else + else if (!SOFT_ERROR_OCCURRED(state->escontext)) { Assert(!*op->resnull); Assert(!fcinfo_in->isnull); @@ -1543,6 +1558,38 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_JSONEXPR_PATH) + { + /* too complex for an inline implementation */ + ExecEvalJsonExpr(state, op, econtext); + EEO_NEXT(); + } + + EEO_CASE(EEOP_JSONEXPR_SKIP) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprSkip(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_BEHAVIOR) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprBehavior(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercion(state, op, econtext, + *op->resvalue, *op->resnull)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION_FINISH) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercionFinish(state, op)); + } + EEO_CASE(EEOP_AGGREF) { /* @@ -3745,7 +3792,7 @@ ExecEvalConstraintCheck(ExprState *state, ExprEvalStep *op) { if (!*op->d.domaincheck.checknull && !DatumGetBool(*op->d.domaincheck.checkvalue)) - ereport(ERROR, + errsave(state->escontext, (errcode(ERRCODE_CHECK_VIOLATION), errmsg("value for domain %s violates check constraint \"%s\"", format_type_be(op->d.domaincheck.resulttype), @@ -4138,6 +4185,457 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op) *op->resvalue = BoolGetDatum(res); } +/* + * Evaluate given JsonExpr by performing the specified JSON operation. + * + * This also populates the JsonExprPostEvalState with the information needed + * by the subsequent steps that handle the specified JsonBehavior. + */ +void +ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) +{ + JsonExprState *jsestate = op->d.jsonexpr.jsestate; + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonExpr *jexpr = jsestate->jsexpr; + Datum item; + Datum res = (Datum) 0; + bool resnull = true; + JsonPath *path; + bool *error; + bool *empty; + + item = pre_eval->formatted_expr.value; + path = DatumGetJsonPathP(pre_eval->pathspec.value); + + /* Reset JsonExprPostEvalState for this evaluation. */ + memset(post_eval, 0, sizeof(*post_eval)); + + /* Respect ON ERROR behavior during path evaluation. */ + jsestate->throw_error = (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR); + + /* + * Be sure to save the error (if needed) and empty statuses for the + * EEOP_JSONEXPR_BEHAVIOR step to peruse. + */ + error = !jsestate->throw_error ? &post_eval->error : NULL; + empty = &post_eval->empty; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + res = JsonPathQuery(item, path, jexpr->wrapper, empty, error, + pre_eval->args); + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + resnull = !DatumGetPointer(res); + break; + + case JSON_VALUE_OP: + { + JsonbValue *jbv = JsonPathValue(item, path, empty, error, + pre_eval->args); + + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + if (!jbv) /* NULL or empty */ + { + resnull = true; + break; + } + + Assert(!*empty); + + resnull = false; + + /* coerce scalar item to the output type */ + if (jexpr->returning->typid == JSONOID || + jexpr->returning->typid == JSONBOID) + { + /* Use result coercion from json[b] to the output type */ + res = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + break; + } + + /* + * Error out if no cast exists to coerce SQL/JSON item to the + * the output type. + */ + Assert(post_eval->item_jcstate == NULL); + res = ExecPrepareJsonItemCoercion(jbv, + jsestate->item_jcstates, + &post_eval->item_jcstate); + if (post_eval->item_jcstate && + post_eval->item_jcstate->coercion && + (post_eval->item_jcstate->coercion->via_io || + post_eval->item_jcstate->coercion->via_populate)) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * Coercion via I/O means here that the cast to the target + * type simply does not exist. + */ + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE), + errmsg("SQL/JSON item cannot be cast to target type"))); + } + break; + } + + case JSON_EXISTS_OP: + { + bool exists = JsonPathExists(item, path, + pre_eval->args, + error); + + resnull = error && *error; + res = BoolGetDatum(exists); + break; + } + + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * If the ON EMPTY behavior is to cause an error, do so here. Other + * behaviors will be handled by the caller. + */ + if (*empty) + { + Assert(jexpr->on_empty); /* it is not JSON_EXISTS */ + + if (jexpr->on_empty->btype == JSON_BEHAVIOR_ERROR) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + ereport(ERROR, + (errcode(ERRCODE_NO_SQL_JSON_ITEM), + errmsg("no SQL/JSON item"))); + } + } + + *op->resvalue = res; + *op->resnull = resnull; +} + +/* + * Skip calling ExecEvalJson() on the given JsonExpr? + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_skip.jsestate; + + /* + * Skip if either of the input expressions has turned out to be NULL, + * though do execute domain checks for NULLs, which are handled by the + * coercion step. + */ + if (jsestate->pre_eval.formatted_expr.isnull || + jsestate->pre_eval.pathspec.isnull) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_skip.jump_coercion; + } + + /* + * Go evaluate the PASSING args if any and subsequently JSON path itself. + */ + return op->d.jsonexpr_skip.jump_passing_args; +} + +/* + * Returns the step address to perform the JsonBehavior applicable to + * the JSON item that resulted from evaluating the given JsonExpr. + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_behavior.jsestate; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonBehavior *behavior = NULL; + int jump_to = -1; + + if (post_eval->error || post_eval->coercion_error) + { + behavior = jsestate->jsexpr->on_error; + jump_to = op->d.jsonexpr_behavior.jump_onerror_expr; + } + else if (post_eval->empty) + { + behavior = jsestate->jsexpr->on_empty; + jump_to = op->d.jsonexpr_behavior.jump_onempty_expr; + } + else if (!post_eval->coercion_done) + { + /* + * If no error or the JSON item is not empty, directly go to the + * coercion step to coerce the item as is. + */ + return op->d.jsonexpr_behavior.jump_coercion; + } + + Assert(behavior); + + /* + * Set up for coercion step that will run to coerce a non-default behavior + * value. It should use result_coercion, if any. Errors that may occur + * should be thrown for JSON ops other than JSON_VALUE_OP. + */ + if (behavior->btype != JSON_BEHAVIOR_DEFAULT) + { + post_eval->item_jcstate = NULL; + jsestate->throw_error = (jsestate->jsexpr->op != JSON_VALUE_OP); + } + + Assert(jump_to >= 0); + return jump_to; +} + +/* + * Evaluate or return the step address to evaluate a coercion of a JSON item + * to the target type. The former if the coercion must be done right away by + * calling its input function or by calling json_populate_type(). + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull) +{ + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonExpr *jexpr = jsestate->jsexpr; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonCoercionState *item_jcstate = post_eval->item_jcstate; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + + if (item_jcstate == NULL && + jsestate->jsexpr->op != JSON_EXISTS_OP) + { + ErrorSaveContext escontext = {T_ErrorSaveContext}; + Node *escontext_p; + JsonCoercion *coercion; + Jsonb *jb; + + escontext_p = !jsestate->throw_error ? (Node *) &escontext : NULL; + coercion = result_jcstate ? result_jcstate->coercion : NULL; + jb = resnull ? NULL : DatumGetJsonbP(res); + + if ((coercion && coercion->via_io) || + (jexpr->omit_quotes && !resnull && + JB_ROOT_IS_SCALAR(jb))) + { + /* strip quotes and call typinput function */ + char *str = resnull ? NULL : JsonbUnquote(jb); + bool type_is_domain; + + type_is_domain = (getBaseType(jexpr->returning->typid) != + jexpr->returning->typid); + + /* + * Catch errors only if the type is not a domain, because errors + * caused by a domain's constraint failure must be thrown right + * away. + */ + if (!InputFunctionCallSafe(jsestate->input.finfo, str, + jsestate->input.typioparam, + jexpr->returning->typmod, + !type_is_domain ? escontext_p : NULL, + op->resvalue)) + { + post_eval->error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + else if (coercion && coercion->via_populate) + { + *op->resvalue = json_populate_type(res, JSONBOID, + jexpr->returning->typid, + jexpr->returning->typmod, + &post_eval->cache, + econtext->ecxt_per_query_memory, + op->resnull, + escontext_p); + if (SOFT_ERROR_OCCURRED(escontext_p)) + { + post_eval->error = true; + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + } + + if (!jsestate->throw_error) + { + post_eval->escontext.type = T_ErrorSaveContext; + state->escontext = (Node *) &post_eval->escontext; + } + + if (item_jcstate && item_jcstate->jump_eval_expr >= 0) + return item_jcstate->jump_eval_expr; + else if (result_jcstate && result_jcstate->jump_eval_expr >= 0) + return result_jcstate->jump_eval_expr; + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; +} + +/* + * Checks if the coercion evaluation led to an error. If an error did occur, + * this returns the address of the step that handles the error, otherwise + * the step after the coercion step, which finishes the JsonExpr processing. + */ +int +ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op) +{ + JsonExprPostEvalState *post_eval = + &op->d.jsonexpr_coercion_finish.jsestate->post_eval; + + if (SOFT_ERROR_OCCURRED(state->escontext)) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + post_eval->coercion_error = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_error; + } + + /* Reset. */ + state->escontext = NULL; + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_done; +} + +/* + * Prepare SQL/JSON item coercion to the output type. Returned a datum of the + * corresponding SQL type and a pointer to the coercion state. + */ +static Datum +ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate) +{ + JsonCoercionState *item_jcstate; + Datum res; + JsonbValue buf; + + if (item->type == jbvBinary && + JsonContainerIsScalar(item->val.binary.data)) + { + bool res PG_USED_FOR_ASSERTS_ONLY; + + res = JsonbExtractScalar(item->val.binary.data, &buf); + item = &buf; + Assert(res); + } + + /* get coercion state reference and datum of the corresponding SQL type */ + switch (item->type) + { + case jbvNull: + item_jcstate = item_jcstates->null; + res = (Datum) 0; + break; + + case jbvString: + item_jcstate = item_jcstates->string; + res = PointerGetDatum(cstring_to_text_with_len(item->val.string.val, + item->val.string.len)); + break; + + case jbvNumeric: + item_jcstate = item_jcstates->numeric; + res = NumericGetDatum(item->val.numeric); + break; + + case jbvBool: + item_jcstate = item_jcstates->boolean; + res = BoolGetDatum(item->val.boolean); + break; + + case jbvDatetime: + res = item->val.datetime.value; + switch (item->val.datetime.typid) + { + case DATEOID: + item_jcstate = item_jcstates->date; + break; + case TIMEOID: + item_jcstate = item_jcstates->time; + break; + case TIMETZOID: + item_jcstate = item_jcstates->timetz; + break; + case TIMESTAMPOID: + item_jcstate = item_jcstates->timestamp; + break; + case TIMESTAMPTZOID: + item_jcstate = item_jcstates->timestamptz; + break; + default: + elog(ERROR, "unexpected jsonb datetime type oid %u", + item->val.datetime.typid); + return (Datum) 0; + } + break; + + case jbvArray: + case jbvObject: + case jbvBinary: + item_jcstate = item_jcstates->composite; + res = JsonbPGetDatum(JsonbValueToJsonb(item)); + break; + + default: + elog(ERROR, "unexpected jsonb value type %d", item->type); + return (Datum) 0; + } + + *p_item_jcstate = item_jcstate; + + return res; +} + /* * ExecEvalGroupingFunc diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 00d7b8110b..da3870cba6 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -1860,6 +1860,256 @@ llvm_compile_expr(ExprState *state) LLVMBuildBr(b, opblocks[opno + 1]); break; + case EEOP_JSONEXPR_PATH: + build_EvalXFunc(b, mod, "ExecEvalJsonExpr", + v_state, op, v_econtext); + LLVMBuildBr(b, opblocks[opno + 1]); + break; + + case EEOP_JSONEXPR_SKIP: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprSkip() to decide if JSON path + * evaluation can be skipped. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprSkip"), + params, lengthof(params), ""); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_skip.jump_coercion, which signifies + * skipping of JSON path evaluation, else to the next step + * which must point to the steps to evaluate PASSING args, + * if any, or to the JSON path evaluation. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_skip.jump_coercion), + ""), + opblocks[op->d.jsonexpr_skip.jump_coercion], + opblocks[opno + 1]); + break; + } + + case EEOP_JSONEXPR_BEHAVIOR: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + LLVMBasicBlockRef b_jump_onerror_default; + + /* + * Call ExecEvalJsonExprBehavior() to decide if ON EMPTY + * or ON ERROR behavior must be invoked depending on what + * JSON path evaluation returned. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprBehavior"), + params, lengthof(params), ""); + + b_jump_onerror_default = + l_bb_before_v(opblocks[opno + 1], + "op.%d.jsonexpr_behavior_jump_onerror_default", opno); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_behavior.jump_coercion, else to the + * next block, one that checks whether to evaluate the ON + * ERROR default expression. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_coercion), + ""), + opblocks[op->d.jsonexpr_behavior.jump_coercion], + b_jump_onerror_default); + + /* + * Block that checks whether to evaluate the ON ERROR + * default expression. + * + * Jump to evaluate the ON ERROR default expression if the + * returned address is the same as + * jsonexpr_behavior.jump_onerror_default, else jump to + * evaluate the ON EMPTY default expression. + */ + LLVMPositionBuilderAtEnd(b, b_jump_onerror_default); + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_onerror_expr), + ""), + opblocks[op->d.jsonexpr_behavior.jump_onerror_expr], + opblocks[op->d.jsonexpr_behavior.jump_onempty_expr]); + break; + } + case EEOP_JSONEXPR_COERCION: + { + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonItemCoercionsState *item_jcstates = jsestate->item_jcstates; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + LLVMValueRef v_ret; + LLVMValueRef params[5]; + + /* + * Call ExecEvalJsonExprCoercion() to evaluate appropriate + * coercion. This will return the step address to jump + * to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + params[2] = v_econtext; + params[3] = v_resvaluep; + params[4] = v_resnullp; + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercion"), + params, lengthof(params), ""); + + /* + * Jump to handle a coercion error if the returned address + * is the same as jsonexpr_coercion.jump_coercion_error, + * else to the step after coercion (coercion done!). + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + + if (item_jcstates) + { + JsonCoercionState **item_jcstate; + int n_coercions = (int) + (item_jcstates->composite - item_jcstates->null) + 1; + int i; + LLVMBasicBlockRef *b_coercions; + + + /* + * Will create a block for each coercion below to + * check whether to evaluate the coercion's expression + * if there's one or to skip to the end if not. + */ + b_coercions = palloc((n_coercions + 1) * sizeof(LLVMBasicBlockRef)); + for (i = 0; i < n_coercions + 1; i++) + b_coercions[i] = + l_bb_before_v(opblocks[opno + 1], + "op.%d.json_item_coercion.%d", + opno, i); + + /* Jump to check first coercion */ + LLVMBuildBr(b, b_coercions[0]); + + /* + * Add conditional branches for individual coercion's + * expressions + */ + for (item_jcstate = &item_jcstates->null, i = 0; + item_jcstate <= &item_jcstates->composite; + item_jcstate++, i++) + { + /* Block for this coercion */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + + /* + * Jump to evaluate the coercion's expression if + * the address returned is the same as this + * coercion's jump_eval_expr (that is, if it is + * valid), else check the next coercion's. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const((*item_jcstate)->jump_eval_expr), + ""), + (*item_jcstate)->jump_eval_expr >= 0 ? + opblocks[(*item_jcstate)->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + b_coercions[i + 1]); + } + + /* + * A placeholder block that the last coercion's block + * might jump to, which unconditionally jumps to end + * of coercions. + */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + LLVMBuildBr(b, opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + + /* + * Jump to evaluate the result_coercion's expression if + * none of the above coercions matched, that is, if + * there's one. + */ + if (result_jcstate) + { + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(result_jcstate->jump_eval_expr), + ""), + result_jcstate->jump_eval_expr >= 0 ? + opblocks[result_jcstate->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + break; + } + + case EEOP_JSONEXPR_COERCION_FINISH: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprCoercionFinish() to check whether + * an coercion error occurred, in which case we must jump + * to whatever step handles the error. This returns the + * step address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercionFinish"), + params, lengthof(params), ""); + + /* + * Jump to the step that handles coercion error if the + * returned address is the same as + * jsonexpr_coercion_finish.jump_coercion_error, else to + * jsonexpr_coercion_finish.jump_coercion_done. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion_finish.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_done]); + break; + } + case EEOP_AGGREF: { LLVMValueRef v_aggno; diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c index 41ac4c6f45..cf3ced3427 100644 --- a/src/backend/jit/llvm/llvmjit_types.c +++ b/src/backend/jit/llvm/llvmjit_types.c @@ -135,6 +135,11 @@ void *referenced_functions[] = ExecEvalXmlExpr, ExecEvalJsonConstructor, ExecEvalJsonIsPredicate, + ExecEvalJsonExprSkip, + ExecEvalJsonExprBehavior, + ExecEvalJsonExprCoercion, + ExecEvalJsonExprCoercionFinish, + ExecEvalJsonExpr, MakeExpandedObjectReadOnlyInternal, slot_getmissingattrs, slot_getsomeattrs_int, diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index c6c310d253..7bcc12b04f 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -863,6 +863,21 @@ makeJsonValueExpr(Expr *expr, JsonFormat *format) return jve; } +/* + * makeJsonBehavior - + * creates a JsonBehavior node + */ +JsonBehavior * +makeJsonBehavior(JsonBehaviorType type, Node *default_expr) +{ + JsonBehavior *behavior = makeNode(JsonBehavior); + + behavior->btype = type; + behavior->default_expr = default_expr; + + return behavior; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index dda964bd19..d31371bb4d 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -236,6 +236,12 @@ exprType(const Node *expr) case T_JsonIsPredicate: type = BOOLOID; break; + case T_JsonExpr: + type = ((const JsonExpr *) expr)->returning->typid; + break; + case T_JsonCoercion: + type = exprType(((const JsonCoercion *) expr)->expr); + break; case T_NullTest: type = BOOLOID; break; @@ -495,6 +501,10 @@ exprTypmod(const Node *expr) return exprTypmod((Node *) ((const JsonValueExpr *) expr)->formatted_expr); case T_JsonConstructorExpr: return ((const JsonConstructorExpr *) expr)->returning->typmod; + case T_JsonExpr: + return ((JsonExpr *) expr)->returning->typmod; + case T_JsonCoercion: + return exprTypmod(((const JsonCoercion *) expr)->expr); case T_CoerceToDomain: return ((const CoerceToDomain *) expr)->resulttypmod; case T_CoerceToDomainValue: @@ -971,6 +981,22 @@ exprCollation(const Node *expr) /* IS JSON's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + coll = InvalidOid; + else if (coercion->expr) + coll = exprCollation(coercion->expr); + else if (coercion->via_io || coercion->via_populate) + coll = coercion->collation; + else + coll = InvalidOid; + } + break; + case T_NullTest: /* NullTest's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ @@ -1207,6 +1233,21 @@ exprSetCollation(Node *expr, Oid collation) case T_JsonIsPredicate: Assert(!OidIsValid(collation)); /* result is always boolean */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + Assert(!OidIsValid(collation)); + else if (coercion->expr) + exprSetCollation(coercion->expr, collation); + else if (coercion->via_io || coercion->via_populate) + coercion->collation = collation; + else + Assert(!OidIsValid(collation)); + } + break; case T_NullTest: /* NullTest's result is boolean ... */ Assert(!OidIsValid(collation)); /* ... so never set a collation */ @@ -1510,6 +1551,15 @@ exprLocation(const Node *expr) case T_JsonIsPredicate: loc = ((const JsonIsPredicate *) expr)->location; break; + case T_JsonExpr: + { + const JsonExpr *jsexpr = (const JsonExpr *) expr; + + /* consider both function name and leftmost arg */ + loc = leftmostLoc(jsexpr->location, + exprLocation(jsexpr->formatted_expr)); + } + break; case T_NullTest: { const NullTest *nexpr = (const NullTest *) expr; @@ -2262,6 +2312,54 @@ expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + if (WALK(jexpr->formatted_expr)) + return true; + if (WALK(jexpr->result_coercion)) + return true; + if (WALK(jexpr->passing_values)) + return true; + /* we assume walker doesn't care about passing_names */ + if (jexpr->on_empty && + WALK(jexpr->on_empty->default_expr)) + return true; + if (WALK(jexpr->on_error->default_expr)) + return true; + if (WALK(jexpr->coercions)) + return true; + } + break; + case T_JsonCoercion: + return WALK(((JsonCoercion *) node)->expr); + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + + if (WALK(coercions->null)) + return true; + if (WALK(coercions->string)) + return true; + if (WALK(coercions->numeric)) + return true; + if (WALK(coercions->boolean)) + return true; + if (WALK(coercions->date)) + return true; + if (WALK(coercions->time)) + return true; + if (WALK(coercions->timetz)) + return true; + if (WALK(coercions->timestamp)) + return true; + if (WALK(coercions->timestamptz)) + return true; + if (WALK(coercions->composite)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: @@ -3261,6 +3359,54 @@ expression_tree_mutator_impl(Node *node, return (Node *) newnode; } + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + JsonExpr *newnode; + + FLATCOPY(newnode, jexpr, JsonExpr); + MUTATE(newnode->path_spec, jexpr->path_spec, Node *); + MUTATE(newnode->formatted_expr, jexpr->formatted_expr, Node *); + MUTATE(newnode->result_coercion, jexpr->result_coercion, JsonCoercion *); + MUTATE(newnode->passing_values, jexpr->passing_values, List *); + /* assume mutator does not care about passing_names */ + if (newnode->on_empty) + MUTATE(newnode->on_empty->default_expr, + jexpr->on_empty->default_expr, Node *); + MUTATE(newnode->on_error->default_expr, + jexpr->on_error->default_expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonCoercion: + { + JsonCoercion *coercion = (JsonCoercion *) node; + JsonCoercion *newnode; + + FLATCOPY(newnode, coercion, JsonCoercion); + MUTATE(newnode->expr, coercion->expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + JsonItemCoercions *newnode; + + FLATCOPY(newnode, coercions, JsonItemCoercions); + MUTATE(newnode->null, coercions->null, JsonCoercion *); + MUTATE(newnode->string, coercions->string, JsonCoercion *); + MUTATE(newnode->numeric, coercions->numeric, JsonCoercion *); + MUTATE(newnode->boolean, coercions->boolean, JsonCoercion *); + MUTATE(newnode->date, coercions->date, JsonCoercion *); + MUTATE(newnode->time, coercions->time, JsonCoercion *); + MUTATE(newnode->timetz, coercions->timetz, JsonCoercion *); + MUTATE(newnode->timestamp, coercions->timestamp, JsonCoercion *); + MUTATE(newnode->timestamptz, coercions->timestamptz, JsonCoercion *); + MUTATE(newnode->composite, coercions->composite, JsonCoercion *); + return (Node *) newnode; + } + break; case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -3947,6 +4093,43 @@ raw_expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonArgument: + return WALK(((JsonArgument *) node)->val); + case T_JsonCommon: + { + JsonCommon *jc = (JsonCommon *) node; + + if (WALK(jc->expr)) + return true; + if (WALK(jc->pathspec)) + return true; + if (WALK(jc->passing)) + return true; + } + break; + case T_JsonBehavior: + { + JsonBehavior *jb = (JsonBehavior *) node; + + if (jb->btype == JSON_BEHAVIOR_DEFAULT && + WALK(jb->default_expr)) + return true; + } + break; + case T_JsonFuncExpr: + { + JsonFuncExpr *jfe = (JsonFuncExpr *) node; + + if (WALK(jfe->common)) + return true; + if (jfe->output && WALK(jfe->output)) + return true; + if (WALK(jfe->on_empty)) + return true; + if (WALK(jfe->on_error)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index ef475d95a1..f58c275b4b 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -4609,7 +4609,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context) IsA(node, SQLValueFunction) || IsA(node, XmlExpr) || IsA(node, CoerceToDomain) || - IsA(node, NextValueExpr)) + IsA(node, NextValueExpr) || + IsA(node, JsonExpr)) { /* Treat all these as having cost 1 */ context->total.per_tuple += cpu_operator_cost; diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index da258968b8..e40cfab4b7 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -53,6 +53,7 @@ #include "utils/fmgroids.h" #include "utils/json.h" #include "utils/jsonb.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/syscache.h" @@ -412,6 +413,24 @@ contain_mutable_functions_walker(Node *node, void *context) /* Check all subnodes */ } + if (IsA(node, JsonExpr)) + { + JsonExpr *jexpr = castNode(JsonExpr, node); + Const *cnst; + + if (!IsA(jexpr->path_spec, Const)) + return true; + + cnst = castNode(Const, jexpr->path_spec); + + Assert(cnst->consttype == JSONPATHOID); + if (cnst->constisnull) + return false; + + return jspIsMutable(DatumGetJsonPathP(cnst->constvalue), + jexpr->passing_names, jexpr->passing_values); + } + if (IsA(node, SQLValueFunction)) { /* all variants of SQLValueFunction are stable */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 341b002dc7..f7ad8f18de 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -278,6 +278,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MergeWhenClause *mergewhen; struct KeyActions *keyactions; struct KeyAction *keyaction; + JsonBehavior *jsbehavior; + JsonQuotes js_quotes; } %type <node> stmt toplevel_stmt schema_stmt routine_body_stmt @@ -650,14 +652,22 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_returning_clause_opt json_name_and_value json_aggregate_func + json_api_common_syntax + json_argument %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt + json_arguments %type <ival> json_encoding_clause_opt json_predicate_type_constraint + json_wrapper_behavior %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt +%type <jsbehavior> json_value_behavior + json_query_behavior + json_exists_behavior +%type <js_quotes> json_quotes_clause_opt /* * Non-keyword token types. These are hard-wired into the "flex" lexer. @@ -694,7 +704,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT - COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT + COMMITTED COMPRESSION CONCURRENTLY CONDITIONAL CONFIGURATION CONFLICT CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY COST CREATE CROSS CSV CUBE CURRENT_P CURRENT_CATALOG CURRENT_DATE CURRENT_ROLE CURRENT_SCHEMA @@ -705,8 +715,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P DOUBLE_P DROP - EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT - EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION + EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE + EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION EXTENSION EXTERNAL EXTRACT FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR @@ -721,10 +731,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION - JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG - JSON_SCALAR JSON_SERIALIZE + JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE - KEY KEYS + KEY KEYS KEEP LABEL LANGUAGE LARGE_P LAST_P LATERAL_P LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL @@ -738,7 +748,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC - OBJECT_P OF OFF OFFSET OIDS OLD ON ONLY OPERATOR OPTION OPTIONS OR + OBJECT_P OF OFF OFFSET OIDS OLD OMIT ON ONLY OPERATOR OPTION OPTIONS OR ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER @@ -747,7 +757,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION - QUOTE + QUOTE QUOTES RANGE READ REAL REASSIGN RECHECK RECURSIVE REF_P REFERENCES REFERENCING REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA @@ -758,7 +768,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); SEQUENCE SEQUENCES SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P - START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P + START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRING_P STRIP_P SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN @@ -766,7 +776,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); TREAT TRIGGER TRIM TRUE_P TRUNCATE TRUSTED TYPE_P TYPES_P - UESCAPE UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN + UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING @@ -15663,6 +15673,192 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_error = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->on_error = $10; + n->location = @1; + $$ = (Node *) n; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->location = @1; + $$ = (Node *) p; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + json_exists_behavior ON ERROR_P + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->on_error = $5; + p->location = @1; + $$ = (Node *) p; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->on_error = $8; + n->location = @1; + $$ = (Node *) n; + } ; @@ -16389,6 +16585,72 @@ opt_asymmetric: ASYMMETRIC ; /* SQL/JSON support */ +json_api_common_syntax: + json_value_expr ',' a_expr /* i.e. a json_path */ + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + + | json_value_expr ',' a_expr /* i.e. a json_path */ + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = $5; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = $7; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_arguments: + json_argument { $$ = list_make1($1); } + | json_arguments ',' json_argument { $$ = lappend($1, $3); } + ; + +json_argument: + json_value_expr AS ColLabel + { + JsonArgument *n = makeNode(JsonArgument); + + n->val = (JsonValueExpr *) $1; + n->name = $3; + $$ = (Node *) n; + } + ; + json_value_expr: a_expr json_format_clause_opt { @@ -16412,6 +16674,50 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; +json_query_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + | EMPTY_P ARRAY { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + | EMPTY_P OBJECT_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + /* non-standard, for Oracle compatibility only */ + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + ; + +json_exists_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | TRUE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_TRUE, NULL); } + | FALSE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_FALSE, NULL); } + | UNKNOWN { $$ = makeJsonBehavior(JSON_BEHAVIOR_UNKNOWN, NULL); } + ; + +json_value_behavior: + NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + ; + +/* ARRAY is a noise word */ +json_wrapper_behavior: + WITHOUT WRAPPER { $$ = JSW_NONE; } + | WITHOUT ARRAY WRAPPER { $$ = JSW_NONE; } + | WITH WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL ARRAY WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL WRAPPER { $$ = JSW_UNCONDITIONAL; } + | /* empty */ { $$ = JSW_NONE; } + ; + +json_quotes_clause_opt: + KEEP QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_KEEP; } + | KEEP QUOTES { $$ = JS_QUOTES_KEEP; } + | OMIT QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_OMIT; } + | OMIT QUOTES { $$ = JS_QUOTES_OMIT; } + | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } + ; + json_returning_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17014,6 +17320,7 @@ unreserved_keyword: | COMMIT | COMMITTED | COMPRESSION + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17050,10 +17357,12 @@ unreserved_keyword: | DOUBLE_P | DROP | EACH + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17103,6 +17412,7 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION + | KEEP | KEY | KEYS | LABEL @@ -17149,6 +17459,7 @@ unreserved_keyword: | OFF | OIDS | OLD + | OMIT | OPERATOR | OPTION | OPTIONS @@ -17179,6 +17490,7 @@ unreserved_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REASSIGN @@ -17238,6 +17550,7 @@ unreserved_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUPPORT @@ -17260,6 +17573,7 @@ unreserved_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNKNOWN | UNLISTEN @@ -17320,10 +17634,13 @@ col_name_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE | LEAST | NATIONAL | NCHAR @@ -17556,6 +17873,7 @@ bare_label_keyword: | COMMITTED | COMPRESSION | CONCURRENTLY + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17608,11 +17926,13 @@ bare_label_keyword: | DROP | EACH | ELSE + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | END_P | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17682,10 +18002,14 @@ bare_label_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE + | KEEP | KEY | KEYS | LABEL @@ -17746,6 +18070,7 @@ bare_label_keyword: | OFF | OIDS | OLD + | OMIT | ONLY | OPERATOR | OPTION @@ -17783,6 +18108,7 @@ bare_label_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REAL @@ -17851,6 +18177,7 @@ bare_label_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUBSTRING @@ -17885,6 +18212,7 @@ bare_label_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNIQUE | UNKNOWN diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c index 9f6afc351c..692e5d1225 100644 --- a/src/backend/parser/parse_collate.c +++ b/src/backend/parser/parse_collate.c @@ -691,6 +691,13 @@ assign_collations_walker(Node *node, assign_collations_context *context) &loccontext); } break; + case T_JsonExpr: + + /* + * Context item and PASSING arguments are already + * marked with collations in parse_expr.c. + */ + break; default: /* diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 198975cb8c..76ce351ecf 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -90,6 +90,7 @@ static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); static Node *transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr); +static Node *transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *p); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -353,6 +354,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); break; + case T_JsonFuncExpr: + result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3225,7 +3230,7 @@ makeJsonByteaToTextConversion(Node *expr, JsonFormat *format, int location) static Node * transformJsonValueExpr(ParseState *pstate, char *constructName, JsonValueExpr *ve, JsonFormatType default_format, - Oid targettype) + Oid targettype, bool isarg) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3262,6 +3267,35 @@ transformJsonValueExpr(ParseState *pstate, char *constructName, else format = ve->format->format_type; } + else if (isarg) + { + /* Pass SQL/JSON item types directly without conversion to json[b]. */ + switch (exprtype) + { + case TEXTOID: + case NUMERICOID: + case BOOLOID: + case INT2OID: + case INT4OID: + case INT8OID: + case FLOAT4OID: + case FLOAT8OID: + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + return expr; + + default: + if (typcategory == TYPCATEGORY_STRING) + return expr; + /* else convert argument to json[b] type */ + break; + } + + format = default_format; + } else if (exprtype == JSONOID || exprtype == JSONBOID) format = JS_FORMAT_DEFAULT; /* do not format json[b] types */ else @@ -3273,7 +3307,8 @@ transformJsonValueExpr(ParseState *pstate, char *constructName, Node *coerced; bool cast_is_needed = OidIsValid(targettype); - if (!cast_is_needed && + if (!isarg && + !cast_is_needed && exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), @@ -3614,7 +3649,7 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) Node *val = transformJsonValueExpr(pstate, "JSON_OBJECT()", kv->value, JS_FORMAT_DEFAULT, - InvalidOid); + InvalidOid, false); args = lappend(args, key); args = lappend(args, val); @@ -3799,7 +3834,7 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) key = transformExprRecurse(pstate, (Node *) agg->arg->key); val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", agg->arg->value, - JS_FORMAT_DEFAULT, InvalidOid); + JS_FORMAT_DEFAULT, InvalidOid, false); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3855,9 +3890,8 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) Oid aggfnoid; Oid aggtype; - arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", - agg->arg, - JS_FORMAT_DEFAULT, InvalidOid); + arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", agg->arg, + JS_FORMAT_DEFAULT, InvalidOid, false); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3904,9 +3938,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) { JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, "JSON_ARRAY()", - jsval, - JS_FORMAT_DEFAULT, - InvalidOid); + jsval, JS_FORMAT_DEFAULT, + InvalidOid, false); args = lappend(args, val); } @@ -4077,7 +4110,7 @@ transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) * function-like CASTs. */ arg = transformJsonValueExpr(pstate, "JSON()", jsexpr->expr, - JS_FORMAT_JSON, returning->typid); + JS_FORMAT_JSON, returning->typid, false); } return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, @@ -4124,9 +4157,8 @@ static Node * transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) { Node *arg = transformJsonValueExpr(pstate, "JSON_SERIALIZE()", - expr->expr, - JS_FORMAT_JSON, - InvalidOid); + expr->expr, JS_FORMAT_JSON, + InvalidOid, false); JsonReturning *returning; if (expr->output) @@ -4161,3 +4193,462 @@ transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), NULL, returning, false, false, expr->location); } + +/* + * Transform a JSON PASSING clause. + */ +static void +transformJsonPassingArgs(ParseState *pstate, char *constructName, + JsonFormatType format, List *args, + List **passing_values, List **passing_names) +{ + ListCell *lc; + + *passing_values = NIL; + *passing_names = NIL; + + foreach(lc, args) + { + JsonArgument *arg = castNode(JsonArgument, lfirst(lc)); + Node *expr = transformJsonValueExpr(pstate, constructName, + arg->val, format, + InvalidOid, true); + + assign_expr_collations(pstate, expr); + + *passing_values = lappend(*passing_values, expr); + *passing_names = lappend(*passing_names, makeString(arg->name)); + } +} + +/* + * Transform a JSON BEHAVIOR clause. + */ +static JsonBehavior * +transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior, + JsonBehaviorType default_behavior) +{ + JsonBehaviorType behavior_type = default_behavior; + Node *default_expr = NULL; + + if (behavior) + { + behavior_type = behavior->btype; + if (behavior_type == JSON_BEHAVIOR_DEFAULT) + default_expr = transformExprRecurse(pstate, behavior->default_expr); + } + return makeJsonBehavior(behavior_type, default_expr); +} + +/* + * Common code for JSON_VALUE, JSON_QUERY, JSON_EXISTS transformation + * into a JsonExpr node. + */ +static JsonExpr * +transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = makeNode(JsonExpr); + Node *pathspec; + JsonFormatType format; + char *constructName; + + if (func->common->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("JSON_TABLE path name is not allowed here"), + parser_errposition(pstate, func->location))); + + jsexpr->location = func->location; + jsexpr->op = func->op; + switch (jsexpr->op) + { + case JSON_VALUE_OP: + constructName = "JSON_VALUE()"; + break; + case JSON_QUERY_OP: + constructName = "JSON_QUERY()"; + break; + case JSON_EXISTS_OP: + constructName = "JSON_EXISTS()"; + break; + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); + break; + } + jsexpr->formatted_expr = transformJsonValueExpr(pstate, constructName, + func->common->expr, + JS_FORMAT_DEFAULT, + InvalidOid, false); + + assign_expr_collations(pstate, jsexpr->formatted_expr); + + /* format is determined by context item type */ + format = exprType(jsexpr->formatted_expr) == JSONBOID ? JS_FORMAT_JSONB : JS_FORMAT_JSON; + + jsexpr->result_coercion = NULL; + jsexpr->omit_quotes = false; + + jsexpr->format = func->common->expr->format; + + pathspec = transformExprRecurse(pstate, func->common->pathspec); + + jsexpr->path_spec = + coerce_to_target_type(pstate, pathspec, exprType(pathspec), + JSONPATHOID, -1, + COERCION_EXPLICIT, COERCE_IMPLICIT_CAST, + exprLocation(pathspec)); + if (!jsexpr->path_spec) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("JSON path expression must be type %s, not type %s", + "jsonpath", format_type_be(exprType(pathspec))), + parser_errposition(pstate, exprLocation(pathspec)))); + + /* transform and coerce to json[b] passing arguments */ + transformJsonPassingArgs(pstate, constructName, format, + func->common->passing, + &jsexpr->passing_values, + &jsexpr->passing_names); + + if (func->op != JSON_EXISTS_OP) + jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, + JSON_BEHAVIOR_NULL); + + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + func->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : + JSON_BEHAVIOR_NULL); + + return jsexpr; +} + +/* + * Assign default JSON returning type from the specified format or from + * the context item type. + */ +static void +assignDefaultJsonReturningType(Node *context_item, JsonFormat *context_format, + JsonReturning *ret) +{ + bool is_jsonb; + + ret->format = copyObject(context_format); + + if (ret->format->format_type == JS_FORMAT_DEFAULT) + is_jsonb = exprType(context_item) == JSONBOID; + else + is_jsonb = ret->format->format_type == JS_FORMAT_JSONB; + + ret->typid = is_jsonb ? JSONBOID : JSONOID; + ret->typmod = -1; +} + +/* + * Try to coerce expression to the output type or + * use json_populate_type() for composite, array and domain types or + * use coercion via I/O. + */ +static JsonCoercion * +coerceJsonExpr(ParseState *pstate, Node *expr, const JsonReturning *returning) +{ + char typtype; + JsonCoercion *coercion = makeNode(JsonCoercion); + + coercion->expr = coerceJsonFuncExpr(pstate, expr, returning, false); + + if (coercion->expr) + { + if (coercion->expr == expr) + coercion->expr = NULL; + + return coercion; + } + + typtype = get_typtype(returning->typid); + + if (returning->typid == RECORDOID || + typtype == TYPTYPE_COMPOSITE || + typtype == TYPTYPE_DOMAIN || + type_is_array(returning->typid)) + coercion->via_populate = true; + else + coercion->via_io = true; + + return coercion; +} + +/* + * Transform a JSON output clause of JSON_VALUE and JSON_QUERY. + */ +static void +transformJsonFuncExprOutput(ParseState *pstate, JsonFuncExpr *func, + JsonExpr *jsexpr) +{ + Node *expr = jsexpr->formatted_expr; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + /* JSON_VALUE returns text by default */ + if (func->op == JSON_VALUE_OP && !OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = TEXTOID; + jsexpr->returning->typmod = -1; + } + + if (OidIsValid(jsexpr->returning->typid)) + { + JsonReturning ret; + + if (func->op == JSON_VALUE_OP && + jsexpr->returning->typid != JSONOID && + jsexpr->returning->typid != JSONBOID) + { + /* Forced coercion via I/O for JSON_VALUE for non-JSON types */ + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = NULL; + jsexpr->result_coercion->via_io = true; + return; + } + + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, &ret); + + if (ret.typid != jsexpr->returning->typid || + ret.typmod != jsexpr->returning->typmod) + { + CaseTestExpr *cte = makeNode(CaseTestExpr); + + cte->typeId = exprType(expr); + cte->typeMod = exprTypmod(expr); + cte->collation = exprCollation(expr); + + Assert(cte->typeId == ret.typid); + Assert(cte->typeMod == ret.typmod); + + jsexpr->result_coercion = coerceJsonExpr(pstate, (Node *) cte, + jsexpr->returning); + } + } + else + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, + jsexpr->returning); +} + +/* + * Coerce an expression in JSON DEFAULT behavior to the target output type. + */ +static Node * +coerceDefaultJsonExpr(ParseState *pstate, JsonExpr *jsexpr, Node *defexpr) +{ + int location; + Oid exprtype; + + if (!defexpr) + return NULL; + + exprtype = exprType(defexpr); + location = exprLocation(defexpr); + + if (location < 0) + location = jsexpr->location; + + defexpr = coerce_to_target_type(pstate, + defexpr, + exprtype, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!defexpr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast DEFAULT expression type %s to %s", + format_type_be(exprtype), + format_type_be(jsexpr->returning->typid)), + parser_errposition(pstate, location))); + + return defexpr; +} + +/* + * Initialize SQL/JSON item coercion from the SQL type "typid" to the target + * "returning" type. + */ +static JsonCoercion * +initJsonItemCoercion(ParseState *pstate, Oid typid, + const JsonReturning *returning) +{ + Node *expr; + + if (typid == UNKNOWNOID) + { + expr = (Node *) makeNullConst(UNKNOWNOID, -1, InvalidOid); + } + else + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + + placeholder->typeId = typid; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + expr = (Node *) placeholder; + } + + return coerceJsonExpr(pstate, expr, returning); +} + +static void +initJsonItemCoercions(ParseState *pstate, JsonItemCoercions *coercions, + const JsonReturning *returning, Oid contextItemTypeId) +{ + struct + { + JsonCoercion **coercion; + Oid typid; + } *p, + coercionTypids[] = + { + {&coercions->null, UNKNOWNOID}, + {&coercions->string, TEXTOID}, + {&coercions->numeric, NUMERICOID}, + {&coercions->boolean, BOOLOID}, + {&coercions->date, DATEOID}, + {&coercions->time, TIMEOID}, + {&coercions->timetz, TIMETZOID}, + {&coercions->timestamp, TIMESTAMPOID}, + {&coercions->timestamptz, TIMESTAMPTZOID}, + {&coercions->composite, contextItemTypeId}, + {NULL, InvalidOid} + }; + + for (p = coercionTypids; p->coercion; p++) + *p->coercion = initJsonItemCoercion(pstate, p->typid, returning); +} + +/* + * Transform JSON_VALUE, JSON_QUERY, JSON_EXISTS functions into a JsonExpr node. + */ +static Node * +transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = transformJsonExprCommon(pstate, func); + const char *func_name = NULL; + Node *contextItemExpr = jsexpr->formatted_expr; + + /* + * Disallow FORMAT specification in the RETURNING clause of JSON_EXISTS() + * and JSON_VALUE(). + */ + if (func->output && + (func->op == JSON_VALUE_OP || func->op == JSON_EXISTS_OP)) + { + JsonFormat *format = func->output->returning->format; + + if(format->format_type != JS_FORMAT_DEFAULT || + format->encoding != JS_ENC_DEFAULT) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot specify FORMAT in RETURNING clause of %s", + func->op == JSON_VALUE_OP ? "JSON_VALUE()" : + "JSON_EXISTS()"), + parser_errposition(pstate, format->location))); + } + + switch (func->op) + { + case JSON_VALUE_OP: + func_name = "JSON_VALUE"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->coercions = makeNode(JsonItemCoercions); + initJsonItemCoercions(pstate, jsexpr->coercions, jsexpr->returning, + exprType(contextItemExpr)); + + break; + + case JSON_QUERY_OP: + func_name = "JSON_QUERY"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->wrapper = func->wrapper; + jsexpr->omit_quotes = func->omit_quotes; + + break; + + case JSON_EXISTS_OP: + func_name = "JSON_EXISTS"; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + if (!OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = BOOLOID; + jsexpr->returning->typmod = -1; + } + else if (jsexpr->returning->typid != BOOLOID) + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + int location = exprLocation((Node *) jsexpr); + + placeholder->typeId = BOOLOID; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = + coerce_to_target_type(pstate, (Node *) placeholder, BOOLOID, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!jsexpr->result_coercion->expr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(BOOLOID), + format_type_be(jsexpr->returning->typid)), + parser_coercion_errposition(pstate, location, (Node *) jsexpr))); + + if (jsexpr->result_coercion->expr == (Node *) placeholder) + jsexpr->result_coercion->expr = NULL; + } + break; + } + + if (exprType(contextItemExpr) != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("%s() is not yet implemented for the json type", func_name), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + return (Node *) jsexpr; +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 520d4f2a23..4f94fc69d6 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1979,6 +1979,21 @@ FigureColnameInternal(Node *node, char **name) /* make JSON_ARRAYAGG act like a regular function */ *name = "json_arrayagg"; return 2; + case T_JsonFuncExpr: + /* make SQL/JSON functions act like a regular function */ + switch (((JsonFuncExpr *) node)->op) + { + case JSON_QUERY_OP: + *name = "json_query"; + return 2; + case JSON_VALUE_OP: + *name = "json_value"; + return 2; + case JSON_EXISTS_OP: + *name = "json_exists"; + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index e27ea8ef97..188b5594e0 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -4426,6 +4426,50 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, } } +/* + * Parses the datetime format string in 'fmt_str' and returns true if it + * contains a timezone specifier, false if not. + */ +bool +datetime_format_has_tz(const char *fmt_str) +{ + bool incache; + int fmt_len = strlen(fmt_str); + int result; + FormatNode *format; + + if (fmt_len > DCH_CACHE_SIZE) + { + /* + * Allocate new memory if format picture is bigger than static cache + * and do not use cache (call parser always) + */ + incache = false; + + format = (FormatNode *) palloc((fmt_len + 1) * sizeof(FormatNode)); + + parse_format(format, fmt_str, DCH_keywords, + DCH_suff, DCH_index, DCH_FLAG, NULL); + } + else + { + /* + * Use cache buffers + */ + DCHCacheEntry *ent = DCH_cache_fetch(fmt_str, false); + + incache = true; + format = ent->format; + } + + result = DCH_datetime_type(format); + + if (!incache) + pfree(format); + + return result & DCH_ZONED; +} + /* * do_to_timestamp: shared code for to_timestamp and to_date * diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index 06ba409e64..d2b4da8ec8 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -2156,3 +2156,65 @@ jsonb_float8(PG_FUNCTION_ARGS) PG_RETURN_DATUM(retValue); } + +/* + * Construct an empty array jsonb. + */ +Jsonb * +JsonbMakeEmptyArray(void) +{ + JsonbValue jbv; + + jbv.type = jbvArray; + jbv.val.array.elems = NULL; + jbv.val.array.nElems = 0; + jbv.val.array.rawScalar = false; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Construct an empty object jsonb. + */ +Jsonb * +JsonbMakeEmptyObject(void) +{ + JsonbValue jbv; + + jbv.type = jbvObject; + jbv.val.object.pairs = NULL; + jbv.val.object.nPairs = 0; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Convert jsonb to a C-string stripping quotes from scalar strings. + */ +char * +JsonbUnquote(Jsonb *jb) +{ + if (JB_ROOT_IS_SCALAR(jb)) + { + JsonbValue v; + + (void) JsonbExtractScalar(&jb->root, &v); + + if (v.type == jbvString) + return pnstrdup(v.val.string.val, v.val.string.len); + else if (v.type == jbvBool) + return pstrdup(v.val.boolean ? "true" : "false"); + else if (v.type == jbvNumeric) + return DatumGetCString(DirectFunctionCall1(numeric_out, + PointerGetDatum(v.val.numeric))); + else if (v.type == jbvNull) + return pstrdup("null"); + else + { + elog(ERROR, "unrecognized jsonb value type %d", v.type); + return NULL; + } + } + else + return JsonbToCString(NULL, &jb->root, VARSIZE(jb)); +} diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index 612bbf06a3..fca72de558 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -265,6 +265,7 @@ typedef struct PopulateArrayContext int *dims; /* dimensions */ int *sizes; /* current dimension counters */ int ndims; /* number of dimensions */ + Node *escontext; /* For soft-error capture */ } PopulateArrayContext; /* state for populate_array_json() */ @@ -442,12 +443,13 @@ static void JsValueToJsObject(JsValue *jsv, JsObject *jso); static Datum populate_composite(CompositeIOData *io, Oid typid, const char *colname, MemoryContext mcxt, HeapTupleHeader defaultval, JsValue *jsv, bool isnull); -static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv); +static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext); static void prepare_column_cache(ColumnIOData *column, Oid typid, int32 typmod, MemoryContext mcxt, bool need_scalar); static Datum populate_record_field(ColumnIOData *col, Oid typid, int32 typmod, const char *colname, MemoryContext mcxt, Datum defaultval, - JsValue *jsv, bool *isnull); + JsValue *jsv, bool *isnull, Node *escontext); static RecordIOData *allocate_record_info(MemoryContext mcxt, int ncolumns); static bool JsObjectGetField(JsObject *obj, char *field, JsValue *jsv); static void populate_recordset_record(PopulateRecordsetState *state, JsObject *obj); @@ -459,7 +461,8 @@ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims); static void populate_array_check_dimension(PopulateArrayContext *ctx, int ndim); static void populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv); static Datum populate_array(ArrayIOData *aio, const char *colname, - MemoryContext mcxt, JsValue *jsv); + MemoryContext mcxt, JsValue *jsv, Node *escontext, + bool *isnull); static Datum populate_domain(DomainIOData *io, Oid typid, const char *colname, MemoryContext mcxt, JsValue *jsv, bool isnull); @@ -2484,12 +2487,12 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) if (ndim <= 0) { if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the value of key \"%s\".", ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"))); } @@ -2506,13 +2509,13 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) appendStringInfo(&indices, "[%d]", ctx->sizes[i]); if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s of key \"%s\".", indices.data, ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s.", @@ -2520,7 +2523,11 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) } } -/* set the number of dimensions of the populated array when it becomes known */ +/* + * Set the number of dimensions of the populated array when it becomes known. + * + * Returns without doing anything if the input (ndims) is erratic. + */ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) { @@ -2531,6 +2538,10 @@ populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) if (ndims <= 0) populate_array_report_expected_array(ctx, ndims); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + ctx->ndims = ndims; ctx->dims = palloc(sizeof(int) * ndims); ctx->sizes = palloc0(sizeof(int) * ndims); @@ -2548,12 +2559,16 @@ populate_array_check_dimension(PopulateArrayContext *ctx, int ndim) if (ctx->dims[ndim] == -1) ctx->dims[ndim] = dim; /* assign dimension if not yet known */ else if (ctx->dims[ndim] != dim) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("malformed JSON array"), errdetail("Multidimensional arrays must have " "sub-arrays with matching dimensions."))); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + /* reset the current array dimension size counter */ ctx->sizes[ndim] = 0; @@ -2573,7 +2588,10 @@ populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv) ctx->aio->element_type, ctx->aio->element_typmod, NULL, ctx->mcxt, PointerGetDatum(NULL), - jsv, &element_isnull); + jsv, &element_isnull, ctx->escontext); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; accumArrayResult(ctx->astate, element, element_isnull, ctx->aio->element_type, ctx->acxt); @@ -2594,6 +2612,10 @@ populate_array_object_start(void *_state) else if (ndim < state->ctx->ndims) populate_array_report_expected_array(state->ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2611,6 +2633,10 @@ populate_array_array_end(void *_state) if (ndim < ctx->ndims) populate_array_check_dimension(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2686,6 +2712,10 @@ populate_array_scalar(void *_state, char *token, JsonTokenType tokentype) else if (ndim < ctx->ndims) populate_array_report_expected_array(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + if (ndim == ctx->ndims) { /* remember the scalar element token */ @@ -2715,7 +2745,13 @@ populate_array_json(PopulateArrayContext *ctx, char *json, int len) sem.array_element_end = populate_array_element_end; sem.scalar = populate_array_scalar; - pg_parse_json_or_ereport(state.lex, &sem); + if (!pg_parse_json_or_errsave(state.lex, &sem, ctx->escontext)) + { + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + pfree(state.lex); + return; + } /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2740,10 +2776,15 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ check_stack_depth(); - if (jbv->type != jbvBinary || !JsonContainerIsArray(jbc)) + if (jbv->type != jbvBinary || + !JsonContainerIsArray(jbc) || + JsonContainerIsScalar(jbc)) + { populate_array_report_expected_array(ctx, ndim - 1); - - Assert(!JsonContainerIsScalar(jbc)); + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + return; + } it = JsonbIteratorInit(jbc); @@ -2762,7 +2803,12 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ (tok == WJB_ELEM && (val.type != jbvBinary || !JsonContainerIsArray(val.val.binary.data))))) + { populate_array_assign_ndims(ctx, ndim); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + } jsv.is_json = false; jsv.val.jsonb = &val; @@ -2780,6 +2826,9 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ { /* populate child sub-array */ populate_array_dim_jsonb(ctx, &val, ndim + 1); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2797,12 +2846,18 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ Assert(tok == WJB_DONE && !it); } -/* recursively populate an array from json/jsonb */ +/* + * Recursively populate an array from json/jsonb + * + * *isnull is set to true if an error is reported during parsing. + */ static Datum populate_array(ArrayIOData *aio, const char *colname, MemoryContext mcxt, - JsValue *jsv) + JsValue *jsv, + Node *escontext, + bool *isnull) { PopulateArrayContext ctx; Datum result; @@ -2817,6 +2872,7 @@ populate_array(ArrayIOData *aio, ctx.ndims = 0; /* unknown yet */ ctx.dims = NULL; ctx.sizes = NULL; + ctx.escontext = escontext; if (jsv->is_json) populate_array_json(&ctx, jsv->val.json.str, @@ -2825,7 +2881,16 @@ populate_array(ArrayIOData *aio, else { populate_array_dim_jsonb(&ctx, jsv->val.jsonb, 1); - ctx.dims[0] = ctx.sizes[0]; + /* Nothing to do on an error. */ + if (!SOFT_ERROR_OCCURRED(ctx.escontext)) + ctx.dims[0] = ctx.sizes[0]; + } + + /* Nothing to return if an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx.escontext)) + { + *isnull = true; + return (Datum) 0; } Assert(ctx.ndims > 0); @@ -2842,6 +2907,7 @@ populate_array(ArrayIOData *aio, pfree(ctx.sizes); pfree(lbs); + *isnull = false; return result; } @@ -2957,7 +3023,8 @@ populate_composite(CompositeIOData *io, /* populate non-null scalar value from json/jsonb value */ static Datum -populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) +populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext) { Datum res; char *str = NULL; @@ -3028,7 +3095,11 @@ populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) elog(ERROR, "unrecognized jsonb type: %d", (int) jbv->type); } - res = InputFunctionCall(&io->typiofunc, str, io->typioparam, typmod); + if (!InputFunctionCallSafe(&io->typiofunc, str, io->typioparam, typmod, + escontext, &res)) + { + res = (Datum) 0; + } /* free temporary buffer */ if (str != json) @@ -3054,7 +3125,7 @@ populate_domain(DomainIOData *io, res = populate_record_field(io->base_io, io->base_typid, io->base_typmod, colname, mcxt, PointerGetDatum(NULL), - jsv, &isnull); + jsv, &isnull, NULL); Assert(!isnull); } @@ -3159,7 +3230,8 @@ populate_record_field(ColumnIOData *col, MemoryContext mcxt, Datum defaultval, JsValue *jsv, - bool *isnull) + bool *isnull, + Node *escontext) { TypeCat typcat; @@ -3192,10 +3264,12 @@ populate_record_field(ColumnIOData *col, switch (typcat) { case TYPECAT_SCALAR: - return populate_scalar(&col->scalar_io, typid, typmod, jsv); + return populate_scalar(&col->scalar_io, typid, typmod, jsv, + escontext); case TYPECAT_ARRAY: - return populate_array(&col->io.array, colname, mcxt, jsv); + return populate_array(&col->io.array, colname, mcxt, jsv, + escontext, isnull); case TYPECAT_COMPOSITE: case TYPECAT_COMPOSITE_DOMAIN: @@ -3216,6 +3290,53 @@ populate_record_field(ColumnIOData *col, } } +/* recursively populate specified type from a json/jsonb value */ +Datum +json_populate_type(Datum json_val, Oid json_type, Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext) +{ + JsValue jsv = {0}; + JsonbValue jbv; + + jsv.is_json = json_type == JSONOID; + + if (*isnull) + { + if (jsv.is_json) + jsv.val.json.str = NULL; + else + jsv.val.jsonb = NULL; + } + else if (jsv.is_json) + { + text *json = DatumGetTextPP(json_val); + + jsv.val.json.str = VARDATA_ANY(json); + jsv.val.json.len = VARSIZE_ANY_EXHDR(json); + jsv.val.json.type = JSON_TOKEN_INVALID; /* not used in + * populate_composite() */ + } + else + { + Jsonb *jsonb = DatumGetJsonbP(json_val); + + jsv.val.jsonb = &jbv; + + /* fill binary jsonb value pointing to jb */ + jbv.type = jbvBinary; + jbv.val.binary.data = &jsonb->root; + jbv.val.binary.len = VARSIZE(jsonb) - VARHDRSZ; + } + + if (!*cache) + *cache = MemoryContextAllocZero(mcxt, sizeof(ColumnIOData)); + + return populate_record_field(*cache, typid, typmod, NULL, mcxt, + PointerGetDatum(NULL), &jsv, isnull, + escontext); +} + static RecordIOData * allocate_record_info(MemoryContext mcxt, int ncolumns) { @@ -3357,7 +3478,8 @@ populate_record(TupleDesc tupdesc, mcxt, nulls[i] ? (Datum) 0 : values[i], &field, - &nulls[i]); + &nulls[i], + NULL); } res = heap_form_tuple(tupdesc, values, nulls); diff --git a/src/backend/utils/adt/jsonpath.c b/src/backend/utils/adt/jsonpath.c index 7891fde310..5194d0d91f 100644 --- a/src/backend/utils/adt/jsonpath.c +++ b/src/backend/utils/adt/jsonpath.c @@ -68,7 +68,9 @@ #include "libpq/pqformat.h" #include "nodes/miscnodes.h" #include "miscadmin.h" +#include "nodes/nodeFuncs.h" #include "utils/builtins.h" +#include "utils/formatting.h" #include "utils/json.h" #include "utils/jsonpath.h" @@ -1109,3 +1111,256 @@ jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, return true; } + +/* SQL/JSON datatype status: */ +typedef enum JsonPathDatatypeStatus +{ + jpdsNonDateTime, /* null, bool, numeric, string, array, object */ + jpdsUnknownDateTime, /* unknown datetime type */ + jpdsDateTimeZoned, /* timetz, timestamptz */ + jpdsDateTimeNonZoned /* time, timestamp, date */ +} JsonPathDatatypeStatus; + +/* Context for jspIsMutableWalker() */ +typedef struct JsonPathMutableContext +{ + List *varnames; /* list of variable names */ + List *varexprs; /* list of variable expressions */ + JsonPathDatatypeStatus current; /* status of @ item */ + bool lax; /* jsonpath is lax or strict */ + bool mutable; /* resulting mutability status */ +} JsonPathMutableContext; + +/* + * Recursive walker for jspIsMutable() + */ +static JsonPathDatatypeStatus +jspIsMutableWalker(JsonPathItem *jpi, JsonPathMutableContext *cxt) +{ + JsonPathItem next; + JsonPathDatatypeStatus status = jpdsNonDateTime; + + while (!cxt->mutable) + { + JsonPathItem arg; + JsonPathDatatypeStatus leftStatus; + JsonPathDatatypeStatus rightStatus; + + switch (jpi->type) + { + case jpiRoot: + Assert(status == jpdsNonDateTime); + break; + + case jpiCurrent: + Assert(status == jpdsNonDateTime); + status = cxt->current; + break; + + case jpiFilter: + { + JsonPathDatatypeStatus prevStatus = cxt->current; + + cxt->current = status; + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + + cxt->current = prevStatus; + break; + } + + case jpiVariable: + { + int32 len; + const char *name = jspGetString(jpi, &len); + ListCell *lc1; + ListCell *lc2; + + Assert(status == jpdsNonDateTime); + + forboth(lc1, cxt->varnames, lc2, cxt->varexprs) + { + String *varname = lfirst_node(String, lc1); + Node *varexpr = lfirst(lc2); + + if (strncmp(varname->sval, name, len)) + continue; + + switch (exprType(varexpr)) + { + case DATEOID: + case TIMEOID: + case TIMESTAMPOID: + status = jpdsDateTimeNonZoned; + break; + + case TIMETZOID: + case TIMESTAMPTZOID: + status = jpdsDateTimeZoned; + break; + + default: + status = jpdsNonDateTime; + break; + } + + break; + } + break; + } + + case jpiEqual: + case jpiNotEqual: + case jpiLess: + case jpiGreater: + case jpiLessOrEqual: + case jpiGreaterOrEqual: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + leftStatus = jspIsMutableWalker(&arg, cxt); + + jspGetRightArg(jpi, &arg); + rightStatus = jspIsMutableWalker(&arg, cxt); + + /* + * Comparison of datetime type with different timezone status + * is mutable. + */ + if (leftStatus != jpdsNonDateTime && + rightStatus != jpdsNonDateTime && + (leftStatus == jpdsUnknownDateTime || + rightStatus == jpdsUnknownDateTime || + leftStatus != rightStatus)) + cxt->mutable = true; + break; + + case jpiNot: + case jpiIsUnknown: + case jpiExists: + case jpiPlus: + case jpiMinus: + Assert(status == jpdsNonDateTime); + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiAnd: + case jpiOr: + case jpiAdd: + case jpiSub: + case jpiMul: + case jpiDiv: + case jpiMod: + case jpiStartsWith: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + jspGetRightArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiIndexArray: + for (int i = 0; i < jpi->content.array.nelems; i++) + { + JsonPathItem from; + JsonPathItem to; + + if (jspGetArraySubscript(jpi, &from, &to, i)) + jspIsMutableWalker(&to, cxt); + + jspIsMutableWalker(&from, cxt); + } + /* FALLTHROUGH */ + + case jpiAnyArray: + if (!cxt->lax) + status = jpdsNonDateTime; + break; + + case jpiAny: + if (jpi->content.anybounds.first > 0) + status = jpdsNonDateTime; + break; + + case jpiDatetime: + if (jpi->content.arg) + { + char *template; + + jspGetArg(jpi, &arg); + if (arg.type != jpiString) + { + status = jpdsNonDateTime; + break; /* there will be runtime error */ + } + + template = jspGetString(&arg, NULL); + if (datetime_format_has_tz(template)) + status = jpdsDateTimeZoned; + else + status = jpdsDateTimeNonZoned; + } + else + { + status = jpdsUnknownDateTime; + } + break; + + case jpiLikeRegex: + Assert(status == jpdsNonDateTime); + jspInitByBuffer(&arg, jpi->base, jpi->content.like_regex.expr); + jspIsMutableWalker(&arg, cxt); + break; + + /* literals */ + case jpiNull: + case jpiString: + case jpiNumeric: + case jpiBool: + /* accessors */ + case jpiKey: + case jpiAnyKey: + /* special items */ + case jpiSubscript: + case jpiLast: + /* item methods */ + case jpiType: + case jpiSize: + case jpiAbs: + case jpiFloor: + case jpiCeiling: + case jpiDouble: + case jpiKeyValue: + status = jpdsNonDateTime; + break; + } + + if (!jspGetNext(jpi, &next)) + break; + + jpi = &next; + } + + return status; +} + +/* + * Check whether jsonpath expression is immutable or not. + */ +bool +jspIsMutable(JsonPath *path, List *varnames, List *varexprs) +{ + JsonPathMutableContext cxt; + JsonPathItem jpi; + + cxt.varnames = varnames; + cxt.varexprs = varexprs; + cxt.current = jpdsNonDateTime; + cxt.lax = (path->header & JSONPATH_LAX) != 0; + cxt.mutable = false; + + jspInit(&jpi, path); + jspIsMutableWalker(&jpi, &cxt); + + return cxt.mutable; +} diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index 2d0599b4aa..eded22e194 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -87,12 +87,16 @@ typedef struct JsonBaseObjectInfo int id; } JsonBaseObjectInfo; +typedef int (*JsonPathVarCallback) (void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); + /* * Context of jsonpath execution. */ typedef struct JsonPathExecContext { - Jsonb *vars; /* variables to substitute into jsonpath */ + void *vars; /* variables to substitute into jsonpath */ + JsonPathVarCallback getVar; JsonbValue *root; /* for $ evaluation */ JsonbValue *current; /* for @ evaluation */ JsonBaseObjectInfo baseObject; /* "base object" for .keyvalue() @@ -174,7 +178,8 @@ typedef JsonPathBool (*JsonPathPredicateCallback) (JsonPathItem *jsp, void *param); typedef Numeric (*BinaryArithmFunc) (Numeric num1, Numeric num2, bool *error); -static JsonPathExecResult executeJsonPath(JsonPath *path, Jsonb *vars, +static JsonPathExecResult executeJsonPath(JsonPath *path, void *vars, + JsonPathVarCallback getVar, Jsonb *json, bool throwErrors, JsonValueList *result, bool useTz); static JsonPathExecResult executeItem(JsonPathExecContext *cxt, @@ -225,8 +230,13 @@ static JsonPathExecResult appendBoolResult(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonValueList *found, JsonPathBool res); static void getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, JsonbValue *value); +static int GetJsonPathVar(void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); static void getJsonPathVariable(JsonPathExecContext *cxt, - JsonPathItem *variable, Jsonb *vars, JsonbValue *value); + JsonPathItem *variable, JsonbValue *value); +static int getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, + int varNameLen, JsonbValue *val, + JsonbValue *baseObject); static int JsonbArraySize(JsonbValue *jb); static JsonPathBool executeComparison(JsonPathItem *cmp, JsonbValue *lv, JsonbValue *rv, void *p); @@ -284,7 +294,8 @@ jsonb_path_exists_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - res = executeJsonPath(jp, vars, jb, !silent, NULL, tz); + res = executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, NULL, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -339,7 +350,8 @@ jsonb_path_match_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -417,7 +429,8 @@ jsonb_path_query_internal(FunctionCallInfo fcinfo, bool tz) vars = PG_GETARG_JSONB_P_COPY(2); silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); funcctx->user_fctx = JsonValueListGetList(&found); @@ -464,7 +477,8 @@ jsonb_path_query_array_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_RETURN_JSONB_P(JsonbValueToJsonb(wrapItemsInArray(&found))); } @@ -495,7 +509,8 @@ jsonb_path_query_first_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); if (JsonValueListLength(&found) >= 1) PG_RETURN_JSONB_P(JsonbValueToJsonb(JsonValueListHead(&found))); @@ -537,8 +552,9 @@ jsonb_path_query_first_tz(PG_FUNCTION_ARGS) * In other case it tries to find all the satisfied result items. */ static JsonPathExecResult -executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, - JsonValueList *result, bool useTz) +executeJsonPath(JsonPath *path, void *vars, JsonPathVarCallback getVar, + Jsonb *json, bool throwErrors, JsonValueList *result, + bool useTz) { JsonPathExecContext cxt; JsonPathExecResult res; @@ -550,22 +566,16 @@ executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, if (!JsonbExtractScalar(&json->root, &jbv)) JsonbInitBinary(&jbv, json); - if (vars && !JsonContainerIsObject(&vars->root)) - { - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"vars\" argument is not an object"), - errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); - } - cxt.vars = vars; + cxt.getVar = getVar; cxt.laxMode = (path->header & JSONPATH_LAX) != 0; cxt.ignoreStructuralErrors = cxt.laxMode; cxt.root = &jbv; cxt.current = &jbv; cxt.baseObject.jbc = NULL; cxt.baseObject.id = 0; - cxt.lastGeneratedObjectId = vars ? 2 : 1; + /* 1 + number of base objects in vars */ + cxt.lastGeneratedObjectId = 1 + getVar(vars, NULL, 0, NULL, NULL); cxt.innermostArraySize = -1; cxt.throwErrors = throwErrors; cxt.useTz = useTz; @@ -2108,54 +2118,118 @@ getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, &value->val.string.len); break; case jpiVariable: - getJsonPathVariable(cxt, item, cxt->vars, value); + getJsonPathVariable(cxt, item, value); return; default: elog(ERROR, "unexpected jsonpath item type"); } } +/* + * Returns the computed value of a JSON path variable with given name. + */ +static int +GetJsonPathVar(void *cxt, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject) +{ + JsonPathVariable *var = NULL; + List *vars = cxt; + ListCell *lc; + int id = 1; + + if (!varName) + return list_length(vars); + + foreach(lc, vars) + { + JsonPathVariable *curvar = lfirst(lc); + + if (!strncmp(curvar->name, varName, varNameLen)) + { + var = curvar; + break; + } + + id++; + } + + if (!var) + return -1; + + if (var->isnull) + { + val->type = jbvNull; + return 0; + } + + JsonItemFromDatum(var->value, var->typid, var->typmod, val); + + *baseObject = *val; + return id; +} + /* * Get the value of variable passed to jsonpath executor */ static void getJsonPathVariable(JsonPathExecContext *cxt, JsonPathItem *variable, - Jsonb *vars, JsonbValue *value) + JsonbValue *value) { char *varName; int varNameLength; + JsonbValue baseObject; + int baseObjectId; + + Assert(variable->type == jpiVariable); + varName = jspGetString(variable, &varNameLength); + + if (!cxt->vars || + (baseObjectId = cxt->getVar(cxt->vars, varName, varNameLength, value, + &baseObject)) < 0) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("could not find jsonpath variable \"%s\"", + pnstrdup(varName, varNameLength)))); + + if (baseObjectId > 0) + setBaseObject(cxt, &baseObject, baseObjectId); +} + +static int +getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, int varNameLength, + JsonbValue *value, JsonbValue *baseObject) +{ + Jsonb *vars = varsJsonb; JsonbValue tmp; JsonbValue *v; - if (!vars) + if (!varName) { - value->type = jbvNull; - return; + if (vars && !JsonContainerIsObject(&vars->root)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"vars\" argument is not an object"), + errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); + } + + return vars ? 1 : 0; /* count of base objects */ } - Assert(variable->type == jpiVariable); - varName = jspGetString(variable, &varNameLength); tmp.type = jbvString; tmp.val.string.val = varName; tmp.val.string.len = varNameLength; v = findJsonbValueFromContainer(&vars->root, JB_FOBJECT, &tmp); - if (v) - { - *value = *v; - pfree(v); - } - else - { - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("could not find jsonpath variable \"%s\"", - pnstrdup(varName, varNameLength)))); - } + if (!v) + return -1; - JsonbInitBinary(&tmp, vars); - setBaseObject(cxt, &tmp, 1); + *value = *v; + pfree(v); + + JsonbInitBinary(baseObject, vars); + return 1; } /**************** Support functions for JsonPath execution *****************/ @@ -2812,3 +2886,240 @@ compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, return DatumGetInt32(DirectFunctionCall2(cmpfunc, val1, val2)); } + +/* Executor-callable JSON_EXISTS implementation */ +bool +JsonPathExists(Datum jb, JsonPath *jp, List *vars, bool *error) +{ + JsonPathExecResult res = executeJsonPath(jp, vars, GetJsonPathVar, + DatumGetJsonbP(jb), !error, NULL, + true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + *error = true; + + return res == jperOk; +} + +/* Executor-callable JSON_QUERY implementation */ +Datum +JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, bool *empty, + bool *error, List *vars) +{ + JsonbValue *first; + bool wrap; + JsonValueList found = {0}; + JsonPathExecResult res PG_USED_FOR_ASSERTS_ONLY; + int count; + + res = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + { + *error = true; + *empty = false; + return (Datum) 0; + } + + count = JsonValueListLength(&found); + + first = count ? JsonValueListHead(&found) : NULL; + + if (!first) + wrap = false; + else if (wrapper == JSW_NONE) + wrap = false; + else if (wrapper == JSW_UNCONDITIONAL) + wrap = true; + else if (wrapper == JSW_CONDITIONAL) + wrap = count > 1 || + IsAJsonbScalar(first) || + (first->type == jbvBinary && + JsonContainerIsScalar(first->val.binary.data)); + else + { + elog(ERROR, "unrecognized json wrapper %d", wrapper); + wrap = false; + } + + if (wrap) + return JsonbPGetDatum(JsonbValueToJsonb(wrapItemsInArray(&found))); + + if (count > 1) + { + if (error) + { + *error = true; + return (Datum) 0; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_QUERY should return singleton item without wrapper"), + errhint("Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array."))); + } + + if (first) + return JsonbPGetDatum(JsonbValueToJsonb(first)); + + *empty = true; + return PointerGetDatum(NULL); +} + +/* Executor-callable JSON_VALUE implementation */ +JsonbValue * +JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars) +{ + JsonbValue *res; + JsonValueList found = {0}; + JsonPathExecResult jper PG_USED_FOR_ASSERTS_ONLY; + int count; + + jper = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(jper)); + + if (error && jperIsError(jper)) + { + *error = true; + *empty = false; + return NULL; + } + + count = JsonValueListLength(&found); + + *empty = !count; + + if (*empty) + return NULL; + + if (count > 1) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + res = JsonValueListHead(&found); + + if (res->type == jbvBinary && + JsonContainerIsScalar(res->val.binary.data)) + JsonbExtractScalar(res->val.binary.data, res); + + if (!IsAJsonbScalar(res)) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_SCALAR_REQUIRED), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + if (res->type == jbvNull) + return NULL; + + return res; +} + +static void +JsonbValueInitNumericDatum(JsonbValue *jbv, Datum num) +{ + jbv->type = jbvNumeric; + jbv->val.numeric = DatumGetNumeric(num); +} + +void +JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) +{ + switch (typid) + { + case BOOLOID: + res->type = jbvBool; + res->val.boolean = DatumGetBool(val); + break; + case NUMERICOID: + JsonbValueInitNumericDatum(res, val); + break; + case INT2OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int2_numeric, val)); + break; + case INT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int4_numeric, val)); + break; + case INT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int8_numeric, val)); + break; + case FLOAT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float4_numeric, val)); + break; + case FLOAT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float8_numeric, val)); + break; + case TEXTOID: + case VARCHAROID: + res->type = jbvString; + res->val.string.val = VARDATA_ANY(val); + res->val.string.len = VARSIZE_ANY_EXHDR(val); + break; + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + res->type = jbvDatetime; + res->val.datetime.value = val; + res->val.datetime.typid = typid; + res->val.datetime.typmod = typmod; + res->val.datetime.tz = 0; + break; + case JSONBOID: + { + JsonbValue *jbv = res; + Jsonb *jb = DatumGetJsonbP(val); + + if (JsonContainerIsScalar(&jb->root)) + { + bool result PG_USED_FOR_ASSERTS_ONLY; + + result = JsonbExtractScalar(&jb->root, jbv); + Assert(result); + } + else + JsonbInitBinary(jbv, jb); + break; + } + case JSONOID: + { + text *txt = DatumGetTextP(val); + char *str = text_to_cstring(txt); + Jsonb *jb; + + jb = DatumGetJsonbP(DirectFunctionCall1(jsonb_in, + CStringGetDatum(str))); + pfree(str); + + JsonItemFromDatum(JsonbPGetDatum(jb), JSONBOID, -1, res); + break; + } + default: + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); + } +} diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d1b03d6cb2..d1ec418ccf 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -476,6 +476,8 @@ static void get_const_expr(Const *constval, deparse_context *context, int showtype); static void get_const_collation(Const *constval, deparse_context *context); static void get_json_format(JsonFormat *format, StringInfo buf); +static void get_json_returning(JsonReturning *returning, StringInfo buf, + bool json_format_by_default); static void get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, bool showimplicit); static void get_json_constructor_options(JsonConstructorExpr *ctor, @@ -518,6 +520,8 @@ static char *generate_qualified_type_name(Oid typid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); +static void get_json_path_spec(Node *path_spec, deparse_context *context, + bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8279,6 +8283,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_WindowFunc: case T_FuncExpr: case T_JsonConstructorExpr: + case T_JsonExpr: /* function-like: name(..) or name[..] */ return true; @@ -8450,6 +8455,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_GroupingFunc: /* own parentheses */ case T_WindowFunc: /* own parentheses */ case T_CaseExpr: /* other separators */ + case T_JsonExpr: /* own parentheses */ return true; default: return false; @@ -8565,6 +8571,65 @@ get_rule_expr_paren(Node *node, deparse_context *context, appendStringInfoChar(context->buf, ')'); } +static void +get_json_behavior(JsonBehavior *behavior, deparse_context *context, + const char *on) +{ + /* + * The order of array elements must correspond to the order of + * JsonBehaviorType members. + */ + const char *behavior_names[] = + { + " NULL", + " ERROR", + " EMPTY", + " TRUE", + " FALSE", + " UNKNOWN", + " EMPTY ARRAY", + " EMPTY OBJECT", + " DEFAULT " + }; + + if ((int) behavior->btype < 0 || behavior->btype >= lengthof(behavior_names)) + elog(ERROR, "invalid json behavior type: %d", behavior->btype); + + appendStringInfoString(context->buf, behavior_names[behavior->btype]); + + if (behavior->btype == JSON_BEHAVIOR_DEFAULT) + get_rule_expr(behavior->default_expr, context, false); + + appendStringInfo(context->buf, " ON %s", on); +} + +/* + * get_json_expr_options + * + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + */ +static void +get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, + JsonBehaviorType default_behavior) +{ + if (jsexpr->op == JSON_QUERY_OP) + { + if (jsexpr->wrapper == JSW_CONDITIONAL) + appendStringInfo(context->buf, " WITH CONDITIONAL WRAPPER"); + else if (jsexpr->wrapper == JSW_UNCONDITIONAL) + appendStringInfo(context->buf, " WITH UNCONDITIONAL WRAPPER"); + + if (jsexpr->omit_quotes) + appendStringInfo(context->buf, " OMIT QUOTES"); + } + + if (jsexpr->op != JSON_EXISTS_OP && + jsexpr->on_empty->btype != default_behavior) + get_json_behavior(jsexpr->on_empty, context, "EMPTY"); + + if (jsexpr->on_error->btype != default_behavior) + get_json_behavior(jsexpr->on_error, context, "ERROR"); +} /* ---------- * get_rule_expr - Parse back an expression @@ -9724,6 +9789,7 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonValueExpr: { JsonValueExpr *jve = (JsonValueExpr *) node; @@ -9773,6 +9839,63 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + appendStringInfoString(buf, "JSON_QUERY("); + break; + case JSON_VALUE_OP: + appendStringInfoString(buf, "JSON_VALUE("); + break; + case JSON_EXISTS_OP: + appendStringInfoString(buf, "JSON_EXISTS("); + break; + } + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_json_path_spec(jexpr->path_spec, context, showimplicit); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoString(buf, " PASSING "); + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + get_rule_expr((Node *) lfirst(lc2), context, showimplicit); + appendStringInfo(buf, " AS %s", + ((String *) lfirst_node(String, lc1))->sval); + } + } + + if (jexpr->op != JSON_EXISTS_OP || + jexpr->returning->typid != BOOLOID) + get_json_returning(jexpr->returning, context->buf, + jexpr->op == JSON_QUERY_OP); + + get_json_expr_options(jexpr, context, + jexpr->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : JSON_BEHAVIOR_NULL); + + appendStringInfoString(buf, ")"); + } + break; + case T_List: { char *sep; @@ -9896,6 +10019,7 @@ looks_like_function(Node *node) case T_MinMaxExpr: case T_SQLValueFunction: case T_XmlExpr: + case T_JsonExpr: /* these are all accepted by func_expr_common_subexpr */ return true; default: @@ -10755,6 +10879,18 @@ get_const_collation(Const *constval, deparse_context *context) } } +/* + * get_json_path_spec - Parse back a JSON path specification + */ +static void +get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit) +{ + if (IsA(path_spec, Const)) + get_const_expr((Const *) path_spec, context, -1); + else + get_rule_expr(path_spec, context, showimplicit); +} + /* * get_json_format - Parse back a JsonFormat node */ diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index 048573c2bc..69fb577850 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -16,12 +16,15 @@ #include "executor/nodeAgg.h" #include "nodes/execnodes.h" +#include "nodes/miscnodes.h" /* forward references to avoid circularity */ struct ExprEvalStep; struct SubscriptingRefState; struct ScalarArrayOpExprHashTable; struct JsonConstructorExprState; +struct JsonbValue; +struct JsonExprState; /* Bits in ExprState->flags (see also execnodes.h for public flag bits): */ /* expression's interpreter has been initialized */ @@ -238,6 +241,11 @@ typedef enum ExprEvalOp EEOP_XMLEXPR, EEOP_JSON_CONSTRUCTOR, EEOP_IS_JSON, + EEOP_JSONEXPR_SKIP, + EEOP_JSONEXPR_PATH, + EEOP_JSONEXPR_BEHAVIOR, + EEOP_JSONEXPR_COERCION, + EEOP_JSONEXPR_COERCION_FINISH, EEOP_AGGREF, EEOP_GROUPING_FUNC, EEOP_WINDOW_FUNC, @@ -689,6 +697,57 @@ typedef struct ExprEvalStep JsonIsPredicate *pred; /* original expression node */ } is_json; + /* for EEOP_JSONEXPR_PATH */ + struct + { + struct JsonExprState *jsestate; + } jsonexpr; + + /* for EEOP_JSONEXPR_SKIP */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprSkip() */ + int jump_coercion; + int jump_passing_args; + } jsonexpr_skip; + + /* for EEOP_JSONEXPR_BEHAVIOR */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprBehavior() */ + int jump_onerror_expr; + int jump_onempty_expr; + int jump_coercion; + int jump_skip_coercion; + } jsonexpr_behavior; + + /* for EEOP_JSONEXPR_COERCION */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion; + + /* for EEOP_JSONEXPR_COERCION_FINISH */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion_finish; } d; } ExprEvalStep; @@ -752,6 +811,111 @@ typedef struct JsonConstructorExprState int nargs; } JsonConstructorExprState; +/* + * Information computed before evaluating a JsonExpr expression. + */ +typedef struct JsonExprPreEvalState +{ + /* value/isnull for JsonExpr.formatted_expr */ + NullableDatum formatted_expr; + + /* value/isnull for JsonExpr.pathspec */ + NullableDatum pathspec; + + /* JsonPathVariable entries for JsonExpr.passing_values */ + List *args; +} JsonExprPreEvalState; + +/* + * State for a given member of JsonItemCoercions. + */ +typedef struct JsonCoercionState +{ + /* Expression used to evaluate the coercion */ + JsonCoercion *coercion; + + /* ExprEvalStep to compute this coercion's expression */ + int jump_eval_expr; +} JsonCoercionState; + +/* + * State for evaluating the coercion for a given JSON item using one of + * the following coercions. + * + * Note that while ExecInitExprRec() for JsonItemCoercions will initialize + * ExprEvalSteps for all of the members that need it, only one will get run + * during a given evaluation of the enclosing JsonExpr depending on the type + * of the result JSON item. + */ +typedef struct JsonItemCoercionsState +{ + JsonCoercionState *null; + JsonCoercionState *string; + JsonCoercionState *numeric; + JsonCoercionState *boolean; + JsonCoercionState *date; + JsonCoercionState *time; + JsonCoercionState *timetz; + JsonCoercionState *timestamp; + JsonCoercionState *timestamptz; + JsonCoercionState *composite; +} JsonItemCoercionsState; + +/* + * State for some post-JsonExpr-evaluation processing steps that gets filled + * in JsonExpr evaluation. + */ +typedef struct JsonExprPostEvalState +{ + /* Is JSON item empty? */ + bool empty; + + /* Did JSON item evaluation cause an error? */ + bool error; + + /* Cache for json_populate_type() called for coercion in some cases */ + void *cache; + + /* + * State for evaluating a JSON item coercion. Points to one of those in + * JsonExprState.coercions chosen by ExecPrepareJsonItemCoercion(). + */ + JsonCoercionState *item_jcstate; + bool coercion_error; + bool coercion_done; + + ErrorSaveContext escontext; +} JsonExprPostEvalState; + +/* EEOP_JSONEXPR state, too big to inline */ +typedef struct JsonExprState +{ + /* original expression node */ + JsonExpr *jsexpr; + + /* + * Should errors be thrown or handled softly? Different parts of + * evaluating a given JsonExpr may require different treatment depending + * on the JsonExprOp; see ExecEvalJsonExprBehavior(). + */ + bool throw_error; + + JsonExprPreEvalState pre_eval; + JsonExprPostEvalState post_eval; + + struct + { + FmgrInfo *finfo; /* typinput function for output type */ + Oid typioparam; + } input; /* I/O info for output type */ + + /* + * Either of the following two is used by ExecEvalJsonExprCoercion() to + * apply coercion to the final result if needed. + */ + JsonCoercionState *result_jcstate; + JsonItemCoercionsState *item_jcstates; +} JsonExprState; /* functions in execExpr.c */ extern void ExprEvalPushStep(ExprState *es, const ExprEvalStep *s); @@ -805,6 +969,14 @@ extern void ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op); extern void ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull); +extern int ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op); +extern void ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, + ExprContext *econtext); extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op); extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op, ExprContext *econtext); diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index ac02247947..089d1c5750 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -17,6 +17,7 @@ #include "executor/execdesc.h" #include "fmgr.h" #include "nodes/lockoptions.h" +#include "nodes/miscnodes.h" #include "nodes/parsenodes.h" #include "utils/memutils.h" diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index cb714f4a19..584bf7001a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -129,6 +129,9 @@ typedef struct ExprState Datum *innermost_domainval; bool *innermost_domainnull; + + /* ErrorSaveContext for soft-error capture. */ + Node *escontext; } ExprState; diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 06d991b725..5e149b0266 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -111,6 +111,7 @@ extern VacuumRelation *makeVacuumRelation(RangeVar *relation, Oid oid, List *va_ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *expr, JsonFormat *format); +extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index d926713bd9..7d63a37d5f 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1727,6 +1727,12 @@ typedef enum JsonQuotes JS_QUOTES_OMIT /* OMIT QUOTES */ } JsonQuotes; +/* + * JsonPathSpec - + * representation of JSON path constant + */ +typedef char *JsonPathSpec; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1738,6 +1744,48 @@ typedef struct JsonOutput JsonReturning *returning; /* RETURNING FORMAT clause and type Oids */ } JsonOutput; +/* + * JsonArgument - + * representation of argument from JSON PASSING clause + */ +typedef struct JsonArgument +{ + NodeTag type; + JsonValueExpr *val; /* argument value expression */ + char *name; /* argument name */ +} JsonArgument; + +/* + * JsonCommon - + * representation of common syntax of functions using JSON path + */ +typedef struct JsonCommon +{ + NodeTag type; + JsonValueExpr *expr; /* context item expression */ + Node *pathspec; /* JSON path specification expression */ + char *pathname; /* path name, if any */ + List *passing; /* list of PASSING clause arguments, if any */ + int location; /* token location, or -1 if unknown */ +} JsonCommon; + +/* + * JsonFuncExpr - + * untransformed representation of JSON function expressions + */ +typedef struct JsonFuncExpr +{ + NodeTag type; + JsonExprOp op; /* expression type */ + JsonCommon *common; /* common syntax */ + JsonOutput *output; /* output clause, if specified */ + JsonBehavior *on_empty; /* ON EMPTY behavior, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + JsonWrapper wrapper; /* array wrapper behavior (JSON_QUERY only) */ + bool omit_quotes; /* omit or keep quotes? (JSON_QUERY only) */ + int location; /* token location, or -1 if unknown */ +} JsonFuncExpr; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 85e484bf43..3937114743 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1544,6 +1544,17 @@ typedef struct XmlExpr int location; } XmlExpr; +/* + * JsonExprOp - + * enumeration of JSON functions using JSON path + */ +typedef enum JsonExprOp +{ + JSON_VALUE_OP, /* JSON_VALUE() */ + JSON_QUERY_OP, /* JSON_QUERY() */ + JSON_EXISTS_OP /* JSON_EXISTS() */ +} JsonExprOp; + /* * JsonEncoding - * representation of JSON ENCODING clause @@ -1568,6 +1579,37 @@ typedef enum JsonFormatType * jsonb */ } JsonFormatType; +/* + * JsonBehaviorType - + * enumeration of behavior types used in JSON ON ... BEHAVIOR clause + * + * If enum members are reordered, get_json_behavior() from ruleutils.c + * must be updated accordingly. + */ +typedef enum JsonBehaviorType +{ + JSON_BEHAVIOR_NULL = 0, + JSON_BEHAVIOR_ERROR, + JSON_BEHAVIOR_EMPTY, + JSON_BEHAVIOR_TRUE, + JSON_BEHAVIOR_FALSE, + JSON_BEHAVIOR_UNKNOWN, + JSON_BEHAVIOR_EMPTY_ARRAY, + JSON_BEHAVIOR_EMPTY_OBJECT, + JSON_BEHAVIOR_DEFAULT +} JsonBehaviorType; + +/* + * JsonWrapper - + * representation of WRAPPER clause for JSON_QUERY() + */ +typedef enum JsonWrapper +{ + JSW_NONE, + JSW_CONDITIONAL, + JSW_UNCONDITIONAL, +} JsonWrapper; + /* * JsonFormat - * representation of JSON FORMAT clause @@ -1661,6 +1703,73 @@ typedef struct JsonIsPredicate int location; /* token location, or -1 if unknown */ } JsonIsPredicate; +/* + * JsonBehavior - + * representation of JSON ON ... BEHAVIOR clause + */ +typedef struct JsonBehavior +{ + NodeTag type; + JsonBehaviorType btype; /* behavior type */ + Node *default_expr; /* default expression, if any */ +} JsonBehavior; + +/* + * JsonCoercion - + * coercion from SQL/JSON item types to SQL types + */ +typedef struct JsonCoercion +{ + NodeTag type; + Node *expr; /* resulting expression coerced to target type */ + bool via_populate; /* coerce result using json_populate_type()? */ + bool via_io; /* coerce result using type input function? */ + Oid collation; /* collation for coercion via I/O or populate */ +} JsonCoercion; + +/* + * JsonItemCoercions - + * expressions for coercion from SQL/JSON item types directly to the + * output SQL type + */ +typedef struct JsonItemCoercions +{ + NodeTag type; + JsonCoercion *null; + JsonCoercion *string; + JsonCoercion *numeric; + JsonCoercion *boolean; + JsonCoercion *date; + JsonCoercion *time; + JsonCoercion *timetz; + JsonCoercion *timestamp; + JsonCoercion *timestamptz; + JsonCoercion *composite; /* arrays and objects */ +} JsonItemCoercions; + +/* + * JsonExpr - + * transformed representation of JSON_VALUE(), JSON_QUERY(), JSON_EXISTS() + */ +typedef struct JsonExpr +{ + Expr xpr; + JsonExprOp op; /* json function ID */ + Node *formatted_expr; /* formatted context item expression */ + JsonCoercion *result_coercion; /* resulting coercion to RETURNING type */ + JsonFormat *format; /* context item format (JSON/JSONB) */ + Node *path_spec; /* JSON path specification expression */ + List *passing_names; /* PASSING argument names */ + List *passing_values; /* PASSING argument values */ + JsonReturning *returning; /* RETURNING clause type/format info */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + JsonItemCoercions *coercions; /* coercions for JSON_VALUE */ + JsonWrapper wrapper; /* WRAPPER for JSON_QUERY */ + bool omit_quotes; /* KEEP/OMIT QUOTES for JSON_QUERY */ + int location; /* token location, or -1 if unknown */ +} JsonExpr; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 5984dcfa4b..0954d9fc7b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -93,6 +93,7 @@ PG_KEYWORD("commit", COMMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("committed", COMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("compression", COMPRESSION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("concurrently", CONCURRENTLY, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("conditional", CONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("configuration", CONFIGURATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("conflict", CONFLICT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("connection", CONNECTION, UNRESERVED_KEYWORD, BARE_LABEL) @@ -147,11 +148,13 @@ PG_KEYWORD("double", DOUBLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("drop", DROP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("each", EACH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("empty", EMPTY_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("event", EVENT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("except", EXCEPT, RESERVED_KEYWORD, AS_LABEL) @@ -233,10 +236,14 @@ PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_exists", JSON_EXISTS, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) @@ -302,6 +309,7 @@ PG_KEYWORD("off", OFF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("offset", OFFSET, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("oids", OIDS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("old", OLD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("omit", OMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("on", ON, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("only", ONLY, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("operator", OPERATOR, UNRESERVED_KEYWORD, BARE_LABEL) @@ -344,6 +352,7 @@ PG_KEYWORD("procedures", PROCEDURES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("publication", PUBLICATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("quotes", QUOTES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("range", RANGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("read", READ, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("real", REAL, COL_NAME_KEYWORD, BARE_LABEL) @@ -414,6 +423,7 @@ PG_KEYWORD("stdout", STDOUT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("storage", STORAGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("stored", STORED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("string", STRING_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("subscription", SUBSCRIPTION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("substring", SUBSTRING, COL_NAME_KEYWORD, BARE_LABEL) @@ -449,6 +459,7 @@ PG_KEYWORD("types", TYPES_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uescape", UESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unbounded", UNBOUNDED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uncommitted", UNCOMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("unconditional", UNCONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unencrypted", UNENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("union", UNION, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("unique", UNIQUE, RESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/formatting.h b/src/include/utils/formatting.h index 0cad3a2709..d4ca0f42ff 100644 --- a/src/include/utils/formatting.h +++ b/src/include/utils/formatting.h @@ -17,7 +17,6 @@ #ifndef _FORMATTING_H_ #define _FORMATTING_H_ - extern char *str_tolower(const char *buff, size_t nbytes, Oid collid); extern char *str_toupper(const char *buff, size_t nbytes, Oid collid); extern char *str_initcap(const char *buff, size_t nbytes, Oid collid); @@ -29,5 +28,6 @@ extern char *asc_initcap(const char *buff, size_t nbytes); extern Datum parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, Oid *typid, int32 *typmod, int *tz, struct Node *escontext); +extern bool datetime_format_has_tz(const char *fmt_str); #endif diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index f928e6142a..e628d4fdd0 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -422,6 +422,9 @@ extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, int estimated_len); +extern Jsonb *JsonbMakeEmptyArray(void); +extern Jsonb *JsonbMakeEmptyObject(void); +extern char *JsonbUnquote(Jsonb *jb); extern bool JsonbExtractScalar(JsonbContainer *jbc, JsonbValue *res); extern const char *JsonbTypeName(JsonbValue *val); diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index 586d948c94..fdc63f26d8 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -15,6 +15,7 @@ #define JSONFUNCS_H #include "common/jsonapi.h" +#include "nodes/nodes.h" #include "utils/jsonb.h" /* @@ -86,5 +87,9 @@ extern Datum to_json_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid); extern Datum to_jsonb_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid); +extern Datum json_populate_type(Datum json_val, Oid json_type, + Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext); #endif diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index f0181e045f..4dae78a98c 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -16,7 +16,9 @@ #include "fmgr.h" #include "nodes/pg_list.h" +#include "nodes/primnodes.h" #include "utils/jsonb.h" +#include "utils/jsonfuncs.h" typedef struct { @@ -184,6 +186,7 @@ extern bool jspGetBool(JsonPathItem *v); extern char *jspGetString(JsonPathItem *v, int32 *len); extern bool jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, int i); +extern bool jspIsMutable(JsonPath *path, List *varnames, List *varexprs); extern const char *jspOperationName(JsonPathItemType type); @@ -261,4 +264,28 @@ extern bool jspConvertRegexFlags(uint32 xflags, int *result, struct Node *escontext); +/* + * Evaluation of jsonpath + */ + +/* External variable passed into jsonpath. */ +typedef struct JsonPathVariable +{ + char *name; + Oid typid; + int32 typmod; + Datum value; + bool isnull; +} JsonPathVariable; + +/* SQL/JSON item */ +extern void JsonItemFromDatum(Datum val, Oid typid, int32 typmod, + JsonbValue *res); + +extern bool JsonPathExists(Datum jb, JsonPath *path, List *vars, bool *error); +extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, + bool *empty, bool *error, List *vars); +extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, + bool *error, List *vars); + #endif diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer index 435c139ec2..b2aa44f36d 100644 --- a/src/interfaces/ecpg/preproc/ecpg.trailer +++ b/src/interfaces/ecpg/preproc/ecpg.trailer @@ -651,6 +651,34 @@ var_type: simple_type $$.type_index = mm_strdup("-1"); $$.type_sizeof = NULL; } + | STRING_P + { + if (INFORMIX_MODE) + { + /* In Informix mode, "string" is automatically a typedef */ + $$.type_enum = ECPGt_string; + $$.type_str = mm_strdup("char"); + $$.type_dimension = mm_strdup("-1"); + $$.type_index = mm_strdup("-1"); + $$.type_sizeof = NULL; + } + else + { + /* Otherwise, legal only if user typedef'ed it */ + struct typedefs *this = get_typedef("string", false); + + $$.type_str = (this->type->type_enum == ECPGt_varchar || this->type->type_enum == ECPGt_bytea) ? EMPTY : mm_strdup(this->name); + $$.type_enum = this->type->type_enum; + $$.type_dimension = this->type->type_dimension; + $$.type_index = this->type->type_index; + if (this->type->type_sizeof && strlen(this->type->type_sizeof) != 0) + $$.type_sizeof = this->type->type_sizeof; + else + $$.type_sizeof = cat_str(3, mm_strdup("sizeof("), mm_strdup(this->name), mm_strdup(")")); + + struct_member_list[struct_level] = ECPGstruct_member_dup(this->struct_member_list); + } + } | INTERVAL ecpg_interval { $$.type_enum = ECPGt_interval; diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out new file mode 100644 index 0000000000..8b87580752 --- /dev/null +++ b/src/test/regress/expected/json_sqljson.out @@ -0,0 +1,18 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); +ERROR: JSON_EXISTS() is not yet implemented for the json type +LINE 1: SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_VALUE +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); +ERROR: JSON_VALUE() is not yet implemented for the json type +LINE 1: SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_QUERY +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); +ERROR: JSON_QUERY() is not yet implemented for the json type +LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out new file mode 100644 index 0000000000..22f8679917 --- /dev/null +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -0,0 +1,1042 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL::jsonb, '$'); + json_exists +------------- + +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb 'null', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_EXISTS(jsonb 'null', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{}', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + json_exists +------------- + f +(1 row) + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + json_exists +------------- + t +(1 row) + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); + json_exists +------------- + 1 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); + json_exists +------------- + 0 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); + json_exists +------------- + true +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +ERROR: cannot cast type boolean to jsonb +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +ERROR: cannot cast type boolean to real +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_EXISTS() +LINE 1: ...CT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSO... + ^ +-- JSON_VALUE +SELECT JSON_VALUE(NULL::jsonb, '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$'); + json_value +------------ + true +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + json_value +------------ + t +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); + json_value +------------ + 123 +(1 row) + +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); +ERROR: SQL/JSON item cannot be cast to target type +SELECT JSON_VALUE(jsonb '1.23', '$'); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "1.23" +SELECT JSON_VALUE(jsonb '"aaa"', '$'); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); + json_value +------------ + aa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "aaa" +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); + json_value +------------ + 111 +(1 row) + +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + ?column? +------------ + 03-01-2017 +(1 row) + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '[]', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '{}', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '1', '$.a'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); + json_value +------------ + error +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 3 +(1 row) + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); + json_value +------------ + 0 +(1 row) + +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: " " +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 5 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_VALUE() +LINE 1: ...CT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSO... + ^ +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + x | y +---+---- + 0 | -2 + 1 | 2 + 2 | -1 +(3 rows) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); + json_value +------------ + (1,2) +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + json_value +------------ + (1,2) +(1 row) + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); + json_value +-------------------------- + Tue Feb 20 18:34:56 2018 +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- JSON_QUERY +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + json_query | json_query | json_query | json_query | json_query +--------------------+--------------------+--------------------+----------------------+---------------------- + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, null, "2"] | [1, null, "2"] | [1, null, "2"] | [[1, null, "2"]] | [[1, null, "2"]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] +(6 rows) + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + unspec | without | with cond | with uncond | with +--------------------+--------------------+---------------------+----------------------+---------------------- + | | | | + | | | | + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, 2, 3] | [1, 2, 3] | [1, 2, 3] | [[1, 2, 3]] | [[1, 2, 3]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] + | | [1, "2", null, [3]] | [1, "2", null, [3]] | [1, "2", null, [3]] +(9 rows) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + json_query +------------ + \x616161 +(1 row) + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTE... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTE... + ^ +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_QUERY should return singleton item without wrapper +HINT: Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array. +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); + json_query +------------ + [1, +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); +ERROR: expected JSON array +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + x | y | list +---+---+-------------- + 0 | 0 | [] + 0 | 1 | [1] + 0 | 2 | [1, 2] + 0 | 3 | [1, 2, 3] + 0 | 4 | [1, 2, 3, 4] + 1 | 0 | [] + 1 | 1 | [1] + 1 | 2 | [1, 2] + 1 | 3 | [1, 2, 3] + 1 | 4 | [1, 2, 3, 4] + 2 | 0 | [] + 2 | 1 | [] + 2 | 2 | [2] + 2 | 3 | [2, 3] + 2 | 4 | [2, 3, 4] + 3 | 0 | [] + 3 | 1 | [] + 3 | 2 | [] + 3 | 3 | [3] + 3 | 4 | [3, 4] + 4 | 0 | [] + 4 | 1 | [] + 4 | 2 | [] + 4 | 3 | [] + 4 | 4 | [4] +(25 rows) + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); + json_query +----------------------------------------------------- + (1,aaa,"[1, ""2"", {}]","{""x"": [1, ""2"", {}]}",) +(1 row) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); + unnest +------------------------ + {"a": 1, "b": ["foo"]} + {"a": 2, "c": {}} + 123 +(3 rows) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); + json_query +-------------- + {1,2,NULL,3} +(1 row) + +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); + json_query +------------ + 1 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- Test constraints +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); +\d test_jsonb_constraints + Table "public.test_jsonb_constraints" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+-------------------------------------------------------------------------------- + js | text | | | + i | integer | | | + x | jsonb | | | JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +Check constraints: + "test_jsonb_constraint1" CHECK (js IS JSON) + "test_jsonb_constraint2" CHECK (JSON_EXISTS(js::jsonb, '$."a"' PASSING i + 5 AS int, i::text AS txt, ARRAY[1, 2, 3] AS arr)) + "test_jsonb_constraint3" CHECK (JSON_VALUE(js::jsonb, '$."a"' RETURNING integer DEFAULT ('12'::text || i)::integer ON EMPTY ERROR ON ERROR) > i) + "test_jsonb_constraint4" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb) + "test_jsonb_constraint5" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C")) + "test_jsonb_constraint6" CHECK (JSON_EXISTS(js::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2) + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + check_clause +-------------------------------------------------------------------------------------------------------------------------- + ((JSON_EXISTS((js)::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2)) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C"))) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb)) + ((JSON_VALUE((js)::jsonb, '$."a"' RETURNING integer DEFAULT (('12'::text || i))::integer ON EMPTY ERROR ON ERROR) > i)) + ((js IS JSON)) + (JSON_EXISTS((js)::jsonb, '$."a"' PASSING (i + 5) AS int, (i)::text AS txt, ARRAY[1, 2, 3] AS arr)) +(6 rows) + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +WHERE constraint_name LIKE 'test_jsonb_constraint%'; +ERROR: syntax error at or near "WHERE" +LINE 1: WHERE constraint_name LIKE 'test_jsonb_constraint%'; + ^ +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint1" +DETAIL: Failing row contains (, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains (1, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('[]'); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ([], null, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ({"b": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint3" +DETAIL: Failing row contains ({"a": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint5" +DETAIL: Failing row contains ({"a": 7}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint4" +DETAIL: Failing row contains ({"a": 10}, 1, [1, 2]). +DROP TABLE test_jsonb_constraints; +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index cf46fa3359..cb3230f4dd 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -103,7 +103,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo # ---------- # Another group of parallel tests (JSON related) # ---------- -test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson +test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson json_sqljson jsonb_sqljson # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql new file mode 100644 index 0000000000..4f30fa46b9 --- /dev/null +++ b/src/test/regress/sql/json_sqljson.sql @@ -0,0 +1,11 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + +-- JSON_QUERY + +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql new file mode 100644 index 0000000000..b8a6148b7b --- /dev/null +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -0,0 +1,327 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL::jsonb, '$'); + +SELECT JSON_EXISTS(jsonb '[]', '$'); +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + +SELECT JSON_EXISTS(jsonb '1', '$'); +SELECT JSON_EXISTS(jsonb 'null', '$'); +SELECT JSON_EXISTS(jsonb '[]', '$'); + +SELECT JSON_EXISTS(jsonb '1', '$.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_EXISTS(jsonb 'null', '$.a'); +SELECT JSON_EXISTS(jsonb '[]', '$.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); +SELECT JSON_EXISTS(jsonb '{}', '$.a'); +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSON); -- RETURNING FORMAT not allowed + + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL::jsonb, '$'); + +SELECT JSON_VALUE(jsonb 'null', '$'); +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + +SELECT JSON_VALUE(jsonb 'true', '$'); +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + +SELECT JSON_VALUE(jsonb '123', '$'); +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1.23', '$'); +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '"aaa"', '$'); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); + +SELECT JSON_VALUE(jsonb '[]', '$'); +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '{}', '$'); +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1', '$.a'); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed + +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- JSON_QUERY + +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); + +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); + +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); + +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- Test constraints + +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); + +\d test_jsonb_constraints + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; +WHERE constraint_name LIKE 'test_jsonb_constraint%'; + +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +INSERT INTO test_jsonb_constraints VALUES ('[]'); +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); + +DROP TABLE test_jsonb_constraints; + +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index d251fb9a91..6e4c026492 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1254,14 +1254,24 @@ JsonArrayAgg JsonArrayConstructor JsonArrayQueryConstructor JsonBaseObjectInfo +JsonBehavior +JsonBehaviorType +JsonCoercion +JsonCoercionState +JsonCommon JsonConstructorExpr JsonConstructorExprState JsonConstructorType JsonEncoding +JsonExpr +JsonExprOp +JsonExprState JsonFormat JsonFormatType JsonHashEntry JsonIsPredicate +JsonItemCoercions +JsonItemCoercionsState JsonIterateStringValuesAction JsonKeyValue JsonLexContext @@ -1294,6 +1304,10 @@ JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback JsonPathString +JsonPathVarCallback +JsonPathVariable +JsonPathVariableEvalContext +JsonQuotes JsonReturning JsonSemAction JsonSerializeExpr -- 2.35.3
v5-0001-Pass-constructName-to-transformJsonValueExpr.patch (application/octet-stream, 3.6K) download From 52afabf6c26da0ebd37e2a81e7a89b8a2a903b78 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Fri, 7 Jul 2023 12:08:58 +0900 Subject: [PATCH v5 1/7] Pass constructName to transformJsonValueExpr() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows it to pass to coerce_to_specific_type() the actual name corresponding to the specific JSON_* function expression being transformed, instead of the currently hardcoded string. Reviewed-by: Álvaro Herrera <alvherre@alvh.no-ip.org> Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- src/backend/parser/parse_expr.c | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 346fd272b6..5bf790cf0f 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -3222,8 +3222,8 @@ makeCaseTestExpr(Node *expr) * default format otherwise. */ static Node * -transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, - JsonFormatType default_format) +transformJsonValueExpr(ParseState *pstate, char *constructName, + JsonValueExpr *ve, JsonFormatType default_format) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3233,12 +3233,8 @@ transformJsonValueExpr(ParseState *pstate, JsonValueExpr *ve, char typcategory; bool typispreferred; - /* - * Using JSON_VALUE here is slightly bogus: perhaps we need to be passed a - * JsonConstructorType so that we can use one of JSON_OBJECTAGG, etc. - */ if (exprType(expr) == UNKNOWNOID) - expr = coerce_to_specific_type(pstate, expr, TEXTOID, "JSON_VALUE"); + expr = coerce_to_specific_type(pstate, expr, TEXTOID, constructName); rawexpr = expr; exprtype = exprType(expr); @@ -3588,7 +3584,8 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) { JsonKeyValue *kv = castNode(JsonKeyValue, lfirst(lc)); Node *key = transformExprRecurse(pstate, (Node *) kv->key); - Node *val = transformJsonValueExpr(pstate, kv->value, + Node *val = transformJsonValueExpr(pstate, "JSON_OBJECT()", + kv->value, JS_FORMAT_DEFAULT); args = lappend(args, key); @@ -3768,7 +3765,9 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) Oid aggtype; key = transformExprRecurse(pstate, (Node *) agg->arg->key); - val = transformJsonValueExpr(pstate, agg->arg->value, JS_FORMAT_DEFAULT); + val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", + agg->arg->value, + JS_FORMAT_DEFAULT); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3824,7 +3823,9 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) Oid aggfnoid; Oid aggtype; - arg = transformJsonValueExpr(pstate, agg->arg, JS_FORMAT_DEFAULT); + arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", + agg->arg, + JS_FORMAT_DEFAULT); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3870,7 +3871,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) foreach(lc, ctor->exprs) { JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); - Node *val = transformJsonValueExpr(pstate, jsval, + Node *val = transformJsonValueExpr(pstate, "JSON_ARRAY()", + jsval, JS_FORMAT_DEFAULT); args = lappend(args, val); -- 2.35.3
v5-0002-Don-t-include-CaseTestExpr-in-JsonValueExpr.forma.patch (application/octet-stream, 8.3K) download From bedf92b475b6ad8b3023a0c3b8975db38c87cff1 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Fri, 7 Jul 2023 20:21:58 +0900 Subject: [PATCH v5 2/7] Don't include CaseTestExpr in JsonValueExpr.formatted_expr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CaseTestExpr is normally (ab)used as a placeholder expression to represent the source value for computing an expression when the expression that provides the source value is computed independently. A CaseTestExpr is currently put into JsonValueExpr.formatted_expr as placeholder for the result of evaluating JsonValueExpr.raw_expr, which in turn is evaluated separately. Though, there's no need for this indirection if raw_expr itself can be embedded into formatted_expr and evaluated as part of evaluating the latter, so this commit makes it so. JsonValueExpr.raw_expr is no longer evaluated by ExecInterpExpr(), eval_const_exprs_mutator(), etc. and only used for displaying the original "unformatted" expression. While at it, this also removes the function makeCaseTestExpr(), because the code in makeJsonConstructorExpr() looks more readable without it, IMO, and isn't used by anyone else either. Finally, a note is added in the comment above CaseTestExpr's definition that JsonConstructorExpr is using it. Reviewed-by: Álvaro Herrera <alvherre@alvh.no-ip.org> Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- src/backend/executor/execExpr.c | 17 ++--------- src/backend/nodes/makefuncs.c | 4 +++ src/backend/optimizer/util/clauses.c | 23 ++++----------- src/backend/parser/parse_expr.c | 43 ++++++++++++++-------------- src/include/nodes/primnodes.h | 7 ++++- 5 files changed, 39 insertions(+), 55 deletions(-) diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index e6e616865c..bf3a08c5f0 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -2294,21 +2294,8 @@ ExecInitExprRec(Expr *node, ExprState *state, { JsonValueExpr *jve = (JsonValueExpr *) node; - ExecInitExprRec(jve->raw_expr, state, resv, resnull); - - if (jve->formatted_expr) - { - Datum *innermost_caseval = state->innermost_caseval; - bool *innermost_isnull = state->innermost_casenull; - - state->innermost_caseval = resv; - state->innermost_casenull = resnull; - - ExecInitExprRec(jve->formatted_expr, state, resv, resnull); - - state->innermost_caseval = innermost_caseval; - state->innermost_casenull = innermost_isnull; - } + Assert(jve->formatted_expr != NULL); + ExecInitExprRec(jve->formatted_expr, state, resv, resnull); break; } diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 39e1884cf4..c6c310d253 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -853,6 +853,10 @@ makeJsonValueExpr(Expr *expr, JsonFormat *format) JsonValueExpr *jve = makeNode(JsonValueExpr); jve->raw_expr = expr; + + /* + * Set after checking the format, if needed, in transformJsonValueExpr(). + */ jve->formatted_expr = NULL; jve->format = format; diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 7f453b04f8..da258968b8 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -2827,25 +2827,12 @@ eval_const_expressions_mutator(Node *node, case T_JsonValueExpr: { JsonValueExpr *jve = (JsonValueExpr *) node; - Node *raw; + Node *formatted; - raw = eval_const_expressions_mutator((Node *) jve->raw_expr, - context); - if (raw && IsA(raw, Const)) - { - Node *formatted; - Node *save_case_val = context->case_val; - - context->case_val = raw; - - formatted = eval_const_expressions_mutator((Node *) jve->formatted_expr, - context); - - context->case_val = save_case_val; - - if (formatted && IsA(formatted, Const)) - return formatted; - } + formatted = eval_const_expressions_mutator((Node *) jve->formatted_expr, + context); + if (formatted && IsA(formatted, Const)) + return formatted; break; } diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 5bf790cf0f..8d7a44408c 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -3202,21 +3202,6 @@ makeJsonByteaToTextConversion(Node *expr, JsonFormat *format, int location) return (Node *) fexpr; } -/* - * Make a CaseTestExpr node. - */ -static Node * -makeCaseTestExpr(Node *expr) -{ - CaseTestExpr *placeholder = makeNode(CaseTestExpr); - - placeholder->typeId = exprType(expr); - placeholder->typeMod = exprTypmod(expr); - placeholder->collation = exprCollation(expr); - - return (Node *) placeholder; -} - /* * Transform JSON value expression using specified input JSON format or * default format otherwise. @@ -3268,11 +3253,8 @@ transformJsonValueExpr(ParseState *pstate, char *constructName, if (format != JS_FORMAT_DEFAULT) { Oid targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; - Node *orig = makeCaseTestExpr(expr); Node *coerced; - expr = orig; - if (exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), @@ -3310,7 +3292,7 @@ transformJsonValueExpr(ParseState *pstate, char *constructName, coerced = (Node *) fexpr; } - if (coerced == orig) + if (coerced == expr) expr = rawexpr; else { @@ -3537,8 +3519,22 @@ makeJsonConstructorExpr(ParseState *pstate, JsonConstructorType type, jsctor->absent_on_null = absent_on_null; jsctor->location = location; + /* + * Coerce to the RETURNING type and format, if needed. We need to abuse + * CaseTestExpr here as placeholder to pass the result of either evaluating + * 'fexpr' or whatever is produced by ExecEvalJsonConstructor() that is of + * type JSON or JSONB to the coercion function. + */ if (fexpr) - placeholder = makeCaseTestExpr((Node *) fexpr); + { + CaseTestExpr *cte = makeNode(CaseTestExpr); + + cte->typeId = exprType((Node *) fexpr); + cte->typeMod = exprTypmod((Node *) fexpr); + cte->collation = exprCollation((Node *) fexpr); + + placeholder = (Node *) cte; + } else { CaseTestExpr *cte = makeNode(CaseTestExpr); @@ -3636,6 +3632,11 @@ transformJsonArrayQueryConstructor(ParseState *pstate, colref->location = ctor->location; agg->arg = makeJsonValueExpr((Expr *) colref, ctor->format); + /* + * No formatting necessary, so set formatted_expr to be the same as + * raw_expr. + */ + agg->arg->formatted_expr = agg->arg->raw_expr; agg->absent_on_null = ctor->absent_on_null; agg->constructor = makeNode(JsonAggConstructor); agg->constructor->agg_order = NIL; @@ -3900,7 +3901,7 @@ transformJsonParseArg(ParseState *pstate, Node *jsexpr, JsonFormat *format, { JsonValueExpr *jve; - expr = makeCaseTestExpr(raw_expr); + expr = raw_expr; expr = makeJsonByteaToTextConversion(expr, format, exprLocation(expr)); *exprtype = TEXTOID; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 792a743f72..0d2df069b3 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1264,6 +1264,8 @@ typedef struct CaseWhen * see build_coercion_expression(). * * Nested FieldStore/SubscriptingRef assignment expressions in INSERT/UPDATE; * see transformAssignmentIndirection(). + * * Placeholder for intermediate results in some SQL/JSON expression nodes, + * such as JsonConstructorExpr. * * The uses in CaseExpr and ArrayCoerceExpr are safe only to the extent that * there is not any other CaseExpr or ArrayCoerceExpr between the value source @@ -1593,12 +1595,15 @@ typedef struct JsonReturning /* * JsonValueExpr - * representation of JSON value expression (expr [FORMAT JsonFormat]) + * + * Note that raw_expr is only there for displaying and is not evaluated by + * ExecInterpExpr() and eval_const_exprs_mutator(). */ typedef struct JsonValueExpr { NodeTag type; Expr *raw_expr; /* raw expression */ - Expr *formatted_expr; /* formatted expression or NULL */ + Expr *formatted_expr; /* formatted expression */ JsonFormat *format; /* FORMAT clause, if specified */ } JsonValueExpr; -- 2.35.3
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-13 03:47  Amit Langote <amitlangote09@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-07-13 03:47 UTC (permalink / raw)
  To: Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: pgsql-hackers; Andrew Dunstan <andrew@dunslane.net>

On Wed, Jul 12, 2023 at 10:23 PM Amit Langote <amitlangote09@gmail.com> wrote:
> On Wed, Jul 12, 2023 at 6:41 PM Amit Langote <amitlangote09@gmail.com> wrote:
> > On Mon, Jul 10, 2023 at 11:52 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
> > > I forgot to add:
> >
> > Thanks for the review of these.
> >
> > > * 0001 looks an obvious improvement.  You could just push it now, to
> > > avoid carrying it forward anymore.  I would just put the constructName
> > > ahead of value expr in the argument list, though.
> >
> > Sure, that makes sense.
> >
> > > * 0002: I have no idea what this is (though I probably should).  I would
> > > also push it right away -- if anything, so that we figure out sooner
> > > that it was actually needed in the first place.  Or maybe you just need
> > > the right test cases?
> >
> > Hmm, I don't think having or not having CaseTestExpr makes a
> > difference to the result of evaluating JsonValueExpr.format_expr, so
> > there are no test cases to prove one way or the other.
> >
> > After staring at this again for a while, I think I figured out why the
> > CaseTestExpr might have been put there in the first place.  It seems
> > to have to do with the fact that JsonValueExpr.raw_expr is currently
> > evaluated independently of JsonValueExpr.formatted_expr and the
> > CaseTestExpr propagates the result of the former to the evaluation of
> > the latter.  Actually, formatted_expr is effectively
> > formatting_function(<result-of-raw_expr>), so if we put raw_expr
> > itself into formatted_expr such that it is evaluated as part of
> > evaluating formatted_expr, then there is no need for the CaseTestExpr
> > as the propagator for raw_expr's result.
> >
> > I've expanded the commit message to mention the details.
> >
> > I'll push these tomorrow.
>
> I updated it to make the code in makeJsonConstructorExpr() that *does*
> need to use a CaseTestExpr a bit more readable.  Also, updated the
> comment above CaseTestExpr to mention this instance of its usage.

Pushed these two just now.

> > On Mon, Jul 10, 2023 at 11:47 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
> > > On 2023-Jul-10, Amit Langote wrote:
> > > > > I'm not in love with the fact that JSON and JSONB have pretty much
> > > > > parallel type categorizing functionality. It seems entirely artificial.
> > > > > Maybe this didn't matter when these were contained inside each .c file
> > > > > and nobody else had to deal with that, but I think it's not good to make
> > > > > this an exported concept.  Is it possible to do away with that?  I mean,
> > > > > reduce both to a single categorization enum, and a single categorization
> > > > > API.  Here you have to cast the enum value to int in order to make
> > > > > ExecInitExprRec work, and that seems a bit lame; moreso when the
> > > > > "is_jsonb" is determined separately (cf. ExecEvalJsonConstructor)
> > > >
> > > > OK, I agree that a unified categorizing API might be better.  I'll
> > > > look at making this better.  Btw, does src/include/common/jsonapi.h
> > > > look like an appropriate place for that?
> > >
> > > Hmm, that header is frontend-available, and the type-category appears to
> > > be backend-only, so maybe no.  Perhaps jsonfuncs.h is more apropos?
> > > execExpr.c is already dealing with array internals, so having to deal
> > > with json internals doesn't seem completely out of place.
> >
> > OK, attached 0003 does it like that.  Essentially, I decided to only
> > keep JsonTypeCategory and json_categorize_type(), with some
> > modifications to accommodate the callers in jsonb.c.
> >
> > > > > In the 2023 standard, JSON_SCALAR is just
> > > > >
> > > > > <JSON scalar> ::= JSON_SCALAR <left paren> <value expression> <right paren>
> > > > >
> > > > > but we seem to have added a <JSON output format> clause to it.  Should
> > > > > we really?
> > > >
> > > > Hmm, I am not seeing <JSON output format> in the rule for JSON_SCALAR,
> > >
> > > Agh, yeah, I confused myself, sorry.
> > >
> > > > Per what I wrote above, the grammar for JSON() and JSON_SCALAR() does
> > > > not allow specifying the FORMAT clause.  Though considering what you
> > > > wrote, the RETURNING clause does appear to be an extension to the
> > > > standard's spec.
> > >
> > > Hmm, I see that <JSON output clause> (which is RETURNING plus optional
> > > FORMAT) appears included in JSON_OBJECT, JSON_ARRAY, JSON_QUERY,
> > > JSON_SERIALIZE, JSON_OBJECTAGG, JSON_ARRAYAGG.  It's not necessarily a
> > > bad thing to have it in other places, but we should consider it
> > > carefully.  Do we really want/need it in JSON() and JSON_SCALAR()?
> >
> > I thought that removing that support breaks JSON_TABLE() or something
> > but it doesn't, so maybe we can do without the extension if there's no
> > particular reason it's there in the first place.  Maybe Andrew (cc'd)
> > remembers why he decided in [1] to (re-) add the RETURNING clause to
> > JSON() and JSON_SCALAR()?
> >
> > Updated patches, with 0003 being a new refactoring patch, are
> > attached.  Patches 0004~ contain a few updates around JsonValueExpr.
> > Specifically, I removed the case for T_JsonValueExpr in
> > transformExprRecurse(), because I realized that JsonValueExpr
> > expressions never appear embedded in other expressions.  That allowed
> > me to get rid of some needless refactoring around
> > transformJsonValueExpr() in the patch that adds JSON_VALUE() etc.
>
> I noticed that 0003 was giving some warnings, which is fixed in the
> attached updated set of patches.

Here are the remaining patches, rebased.  I'll remove the RETURNING
clause from JSON() and JSON_SCALAR() in the next version that I will
post tomorrow unless I hear objections.

--
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

Attachments: download all (5 files, .tar.gz)

v6-0001-Unify-JSON-categorize-type-API-and-export-for-ext.patch (application/octet-stream, 27.5K) download From 7c050e9a00930ab0a4d7381529ab37606575d898 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Tue, 11 Jul 2023 16:00:42 +0900 Subject: [PATCH v6 1/5] Unify JSON categorize type API and export for external use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This essentially removes the JsonbTypeCategory enum and jsonb_categorize_type() and integrates any jsonb-specific logic that was in jsonb_categorize_type() into json_categorize_type(), now moved to jsonfuncs.c, such that it covers the needs of the callers in both json.c and jsonb.c. The common JsonTypeCategory enum is also exported in jsonfuncs.h. json_categorize_type() has grown a new parameter named is_jsonb for callers in jsonb.c to engage the jsonb-specific behavior of json_categorize_type(). Reviewed-by: Álvaro Herrera <alvherre@alvh.no-ip.org> Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- src/backend/utils/adt/json.c | 137 ++--------------- src/backend/utils/adt/jsonb.c | 245 +++++++----------------------- src/backend/utils/adt/jsonfuncs.c | 111 ++++++++++++++ src/include/utils/jsonfuncs.h | 20 +++ src/tools/pgindent/typedefs.list | 1 - 5 files changed, 199 insertions(+), 315 deletions(-) diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index 49080e5fbf..f6bef9c148 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -19,7 +19,6 @@ #include "funcapi.h" #include "libpq/pqformat.h" #include "miscadmin.h" -#include "parser/parse_coerce.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/date.h" @@ -29,21 +28,6 @@ #include "utils/lsyscache.h" #include "utils/typcache.h" -typedef enum /* type categories for datum_to_json */ -{ - JSONTYPE_NULL, /* null, so we didn't bother to identify */ - JSONTYPE_BOOL, /* boolean (built-in types only) */ - JSONTYPE_NUMERIC, /* numeric (ditto) */ - JSONTYPE_DATE, /* we use special formatting for datetimes */ - JSONTYPE_TIMESTAMP, - JSONTYPE_TIMESTAMPTZ, - JSONTYPE_JSON, /* JSON itself (and JSONB) */ - JSONTYPE_ARRAY, /* array */ - JSONTYPE_COMPOSITE, /* composite */ - JSONTYPE_CAST, /* something with an explicit cast to JSON */ - JSONTYPE_OTHER /* all else */ -} JsonTypeCategory; - /* * Support for fast key uniqueness checking. @@ -107,9 +91,6 @@ static void array_dim_to_json(StringInfo result, int dim, int ndims, int *dims, bool use_line_feeds); static void array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds); -static void json_categorize_type(Oid typoid, - JsonTypeCategory *tcategory, - Oid *outfuncoid); static void datum_to_json(Datum val, bool is_null, StringInfo result, JsonTypeCategory tcategory, Oid outfuncoid, bool key_scalar); @@ -182,106 +163,6 @@ json_recv(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(cstring_to_text_with_len(str, nbytes)); } -/* - * Determine how we want to print values of a given type in datum_to_json. - * - * Given the datatype OID, return its JsonTypeCategory, as well as the type's - * output function OID. If the returned category is JSONTYPE_CAST, we - * return the OID of the type->JSON cast function instead. - */ -static void -json_categorize_type(Oid typoid, - JsonTypeCategory *tcategory, - Oid *outfuncoid) -{ - bool typisvarlena; - - /* Look through any domain */ - typoid = getBaseType(typoid); - - *outfuncoid = InvalidOid; - - /* - * We need to get the output function for everything except date and - * timestamp types, array and composite types, booleans, and non-builtin - * types where there's a cast to json. - */ - - switch (typoid) - { - case BOOLOID: - *tcategory = JSONTYPE_BOOL; - break; - - case INT2OID: - case INT4OID: - case INT8OID: - case FLOAT4OID: - case FLOAT8OID: - case NUMERICOID: - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - *tcategory = JSONTYPE_NUMERIC; - break; - - case DATEOID: - *tcategory = JSONTYPE_DATE; - break; - - case TIMESTAMPOID: - *tcategory = JSONTYPE_TIMESTAMP; - break; - - case TIMESTAMPTZOID: - *tcategory = JSONTYPE_TIMESTAMPTZ; - break; - - case JSONOID: - case JSONBOID: - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - *tcategory = JSONTYPE_JSON; - break; - - default: - /* Check for arrays and composites */ - if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID - || typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID) - *tcategory = JSONTYPE_ARRAY; - else if (type_is_rowtype(typoid)) /* includes RECORDOID */ - *tcategory = JSONTYPE_COMPOSITE; - else - { - /* It's probably the general case ... */ - *tcategory = JSONTYPE_OTHER; - /* but let's look for a cast to json, if it's not built-in */ - if (typoid >= FirstNormalObjectId) - { - Oid castfunc; - CoercionPathType ctype; - - ctype = find_coercion_pathway(JSONOID, typoid, - COERCION_EXPLICIT, - &castfunc); - if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc)) - { - *tcategory = JSONTYPE_CAST; - *outfuncoid = castfunc; - } - else - { - /* non builtin type with no cast */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - } - else - { - /* any other builtin type */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - } - break; - } -} - /* * Turn a Datum into JSON text, appending the string to "result". * @@ -591,7 +472,7 @@ array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds) get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign); - json_categorize_type(element_type, + json_categorize_type(element_type, false, &tcategory, &outfuncoid); deconstruct_array(v, element_type, typlen, typbyval, @@ -665,7 +546,8 @@ composite_to_json(Datum composite, StringInfo result, bool use_line_feeds) outfuncoid = InvalidOid; } else - json_categorize_type(att->atttypid, &tcategory, &outfuncoid); + json_categorize_type(att->atttypid, false, &tcategory, + &outfuncoid); datum_to_json(val, isnull, result, tcategory, outfuncoid, false); } @@ -699,7 +581,7 @@ add_json(Datum val, bool is_null, StringInfo result, outfuncoid = InvalidOid; } else - json_categorize_type(val_type, + json_categorize_type(val_type, false, &tcategory, &outfuncoid); datum_to_json(val, is_null, result, tcategory, outfuncoid, key_scalar); @@ -784,12 +666,13 @@ to_json_is_immutable(Oid typoid) JsonTypeCategory tcategory; Oid outfuncoid; - json_categorize_type(typoid, &tcategory, &outfuncoid); + json_categorize_type(typoid, false, &tcategory, &outfuncoid); switch (tcategory) { case JSONTYPE_BOOL: case JSONTYPE_JSON: + case JSONTYPE_JSONB: case JSONTYPE_NULL: return true; @@ -830,7 +713,7 @@ to_json(PG_FUNCTION_ARGS) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - json_categorize_type(val_type, + json_categorize_type(val_type, false, &tcategory, &outfuncoid); result = makeStringInfo(); @@ -880,7 +763,7 @@ json_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null) MemoryContextSwitchTo(oldcontext); appendStringInfoChar(state->str, '['); - json_categorize_type(arg_type, &state->val_category, + json_categorize_type(arg_type, false, &state->val_category, &state->val_output_func); } else @@ -1112,7 +995,7 @@ json_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine data type for argument %d", 1))); - json_categorize_type(arg_type, &state->key_category, + json_categorize_type(arg_type, false, &state->key_category, &state->key_output_func); arg_type = get_fn_expr_argtype(fcinfo->flinfo, 2); @@ -1122,7 +1005,7 @@ json_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine data type for argument %d", 2))); - json_categorize_type(arg_type, &state->val_category, + json_categorize_type(arg_type, false, &state->val_category, &state->val_output_func); appendStringInfoString(state->str, "{ "); diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index cf43c3f2de..fc64f56868 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -19,7 +19,6 @@ #include "funcapi.h" #include "libpq/pqformat.h" #include "miscadmin.h" -#include "parser/parse_coerce.h" #include "utils/builtins.h" #include "utils/date.h" #include "utils/datetime.h" @@ -37,29 +36,12 @@ typedef struct JsonbInState Node *escontext; } JsonbInState; -/* unlike with json categories, we need to treat json and jsonb differently */ -typedef enum /* type categories for datum_to_jsonb */ -{ - JSONBTYPE_NULL, /* null, so we didn't bother to identify */ - JSONBTYPE_BOOL, /* boolean (built-in types only) */ - JSONBTYPE_NUMERIC, /* numeric (ditto) */ - JSONBTYPE_DATE, /* we use special formatting for datetimes */ - JSONBTYPE_TIMESTAMP, /* we use special formatting for timestamp */ - JSONBTYPE_TIMESTAMPTZ, /* ... and timestamptz */ - JSONBTYPE_JSON, /* JSON */ - JSONBTYPE_JSONB, /* JSONB */ - JSONBTYPE_ARRAY, /* array */ - JSONBTYPE_COMPOSITE, /* composite */ - JSONBTYPE_JSONCAST, /* something with an explicit cast to JSON */ - JSONBTYPE_OTHER /* all else */ -} JsonbTypeCategory; - typedef struct JsonbAggState { JsonbInState *res; - JsonbTypeCategory key_category; + JsonTypeCategory key_category; Oid key_output_func; - JsonbTypeCategory val_category; + JsonTypeCategory val_category; Oid val_output_func; } JsonbAggState; @@ -72,19 +54,13 @@ static JsonParseErrorType jsonb_in_array_end(void *pstate); static JsonParseErrorType jsonb_in_object_field_start(void *pstate, char *fname, bool isnull); static void jsonb_put_escaped_value(StringInfo out, JsonbValue *scalarVal); static JsonParseErrorType jsonb_in_scalar(void *pstate, char *token, JsonTokenType tokentype); -static void jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid); static void composite_to_jsonb(Datum composite, JsonbInState *result); static void array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *dims, Datum *vals, bool *nulls, int *valcount, - JsonbTypeCategory tcategory, Oid outfuncoid); + JsonTypeCategory tcategory, Oid outfuncoid); static void array_to_jsonb_internal(Datum array, JsonbInState *result); -static void jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid); static void datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, - JsonbTypeCategory tcategory, Oid outfuncoid, + JsonTypeCategory tcategory, Oid outfuncoid, bool key_scalar); static void add_jsonb(Datum val, bool is_null, JsonbInState *result, Oid val_type, bool key_scalar); @@ -633,112 +609,6 @@ add_indent(StringInfo out, bool indent, int level) } -/* - * Determine how we want to render values of a given type in datum_to_jsonb. - * - * Given the datatype OID, return its JsonbTypeCategory, as well as the type's - * output function OID. If the returned category is JSONBTYPE_JSONCAST, - * we return the OID of the relevant cast function instead. - */ -static void -jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid) -{ - bool typisvarlena; - - /* Look through any domain */ - typoid = getBaseType(typoid); - - *outfuncoid = InvalidOid; - - /* - * We need to get the output function for everything except date and - * timestamp types, booleans, array and composite types, json and jsonb, - * and non-builtin types where there's a cast to json. In this last case - * we return the oid of the cast function instead. - */ - - switch (typoid) - { - case BOOLOID: - *tcategory = JSONBTYPE_BOOL; - break; - - case INT2OID: - case INT4OID: - case INT8OID: - case FLOAT4OID: - case FLOAT8OID: - case NUMERICOID: - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - *tcategory = JSONBTYPE_NUMERIC; - break; - - case DATEOID: - *tcategory = JSONBTYPE_DATE; - break; - - case TIMESTAMPOID: - *tcategory = JSONBTYPE_TIMESTAMP; - break; - - case TIMESTAMPTZOID: - *tcategory = JSONBTYPE_TIMESTAMPTZ; - break; - - case JSONBOID: - *tcategory = JSONBTYPE_JSONB; - break; - - case JSONOID: - *tcategory = JSONBTYPE_JSON; - break; - - default: - /* Check for arrays and composites */ - if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID - || typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID) - *tcategory = JSONBTYPE_ARRAY; - else if (type_is_rowtype(typoid)) /* includes RECORDOID */ - *tcategory = JSONBTYPE_COMPOSITE; - else - { - /* It's probably the general case ... */ - *tcategory = JSONBTYPE_OTHER; - - /* - * but first let's look for a cast to json (note: not to - * jsonb) if it's not built-in. - */ - if (typoid >= FirstNormalObjectId) - { - Oid castfunc; - CoercionPathType ctype; - - ctype = find_coercion_pathway(JSONOID, typoid, - COERCION_EXPLICIT, &castfunc); - if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc)) - { - *tcategory = JSONBTYPE_JSONCAST; - *outfuncoid = castfunc; - } - else - { - /* not a cast type, so just get the usual output func */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - } - else - { - /* any other builtin type */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - break; - } - } -} - /* * Turn a Datum into jsonb, adding it to the result JsonbInState. * @@ -753,7 +623,7 @@ jsonb_categorize_type(Oid typoid, */ static void datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, - JsonbTypeCategory tcategory, Oid outfuncoid, + JsonTypeCategory tcategory, Oid outfuncoid, bool key_scalar) { char *outputstr; @@ -770,11 +640,11 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, jb.type = jbvNull; } else if (key_scalar && - (tcategory == JSONBTYPE_ARRAY || - tcategory == JSONBTYPE_COMPOSITE || - tcategory == JSONBTYPE_JSON || - tcategory == JSONBTYPE_JSONB || - tcategory == JSONBTYPE_JSONCAST)) + (tcategory == JSONTYPE_ARRAY || + tcategory == JSONTYPE_COMPOSITE || + tcategory == JSONTYPE_JSON || + tcategory == JSONTYPE_JSONB || + tcategory == JSONTYPE_JSON)) { ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -782,18 +652,18 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, } else { - if (tcategory == JSONBTYPE_JSONCAST) + if (tcategory == JSONTYPE_CAST) val = OidFunctionCall1(outfuncoid, val); switch (tcategory) { - case JSONBTYPE_ARRAY: + case JSONTYPE_ARRAY: array_to_jsonb_internal(val, result); break; - case JSONBTYPE_COMPOSITE: + case JSONTYPE_COMPOSITE: composite_to_jsonb(val, result); break; - case JSONBTYPE_BOOL: + case JSONTYPE_BOOL: if (key_scalar) { outputstr = DatumGetBool(val) ? "true" : "false"; @@ -807,7 +677,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, jb.val.boolean = DatumGetBool(val); } break; - case JSONBTYPE_NUMERIC: + case JSONTYPE_NUMERIC: outputstr = OidOutputFunctionCall(outfuncoid, val); if (key_scalar) { @@ -845,26 +715,26 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, } } break; - case JSONBTYPE_DATE: + case JSONTYPE_DATE: jb.type = jbvString; jb.val.string.val = JsonEncodeDateTime(NULL, val, DATEOID, NULL); jb.val.string.len = strlen(jb.val.string.val); break; - case JSONBTYPE_TIMESTAMP: + case JSONTYPE_TIMESTAMP: jb.type = jbvString; jb.val.string.val = JsonEncodeDateTime(NULL, val, TIMESTAMPOID, NULL); jb.val.string.len = strlen(jb.val.string.val); break; - case JSONBTYPE_TIMESTAMPTZ: + case JSONTYPE_TIMESTAMPTZ: jb.type = jbvString; jb.val.string.val = JsonEncodeDateTime(NULL, val, TIMESTAMPTZOID, NULL); jb.val.string.len = strlen(jb.val.string.val); break; - case JSONBTYPE_JSONCAST: - case JSONBTYPE_JSON: + case JSONTYPE_CAST: + case JSONTYPE_JSON: { /* parse the json right into the existing result object */ JsonLexContext *lex; @@ -887,7 +757,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, pg_parse_json_or_ereport(lex, &sem); } break; - case JSONBTYPE_JSONB: + case JSONTYPE_JSONB: { Jsonb *jsonb = DatumGetJsonbP(val); JsonbIterator *it; @@ -931,7 +801,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, /* Now insert jb into result, unless we did it recursively */ if (!is_null && !scalar_jsonb && - tcategory >= JSONBTYPE_JSON && tcategory <= JSONBTYPE_JSONCAST) + tcategory >= JSONTYPE_JSON && tcategory <= JSONTYPE_CAST) { /* work has been done recursively */ return; @@ -976,7 +846,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, */ static void array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *dims, Datum *vals, - bool *nulls, int *valcount, JsonbTypeCategory tcategory, + bool *nulls, int *valcount, JsonTypeCategory tcategory, Oid outfuncoid) { int i; @@ -1020,7 +890,7 @@ array_to_jsonb_internal(Datum array, JsonbInState *result) int16 typlen; bool typbyval; char typalign; - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; ndim = ARR_NDIM(v); @@ -1037,8 +907,8 @@ array_to_jsonb_internal(Datum array, JsonbInState *result) get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign); - jsonb_categorize_type(element_type, - &tcategory, &outfuncoid); + json_categorize_type(element_type, true, + &tcategory, &outfuncoid); deconstruct_array(v, element_type, typlen, typbyval, typalign, &elements, &nulls, @@ -1084,7 +954,7 @@ composite_to_jsonb(Datum composite, JsonbInState *result) Datum val; bool isnull; char *attname; - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; JsonbValue v; Form_pg_attribute att = TupleDescAttr(tupdesc, i); @@ -1105,11 +975,12 @@ composite_to_jsonb(Datum composite, JsonbInState *result) if (isnull) { - tcategory = JSONBTYPE_NULL; + tcategory = JSONTYPE_NULL; outfuncoid = InvalidOid; } else - jsonb_categorize_type(att->atttypid, &tcategory, &outfuncoid); + json_categorize_type(att->atttypid, true, &tcategory, + &outfuncoid); datum_to_jsonb(val, isnull, result, tcategory, outfuncoid, false); } @@ -1122,7 +993,7 @@ composite_to_jsonb(Datum composite, JsonbInState *result) * Append JSON text for "val" to "result". * * This is just a thin wrapper around datum_to_jsonb. If the same type will be - * printed many times, avoid using this; better to do the jsonb_categorize_type + * printed many times, avoid using this; better to do the json_categorize_type * lookups only once. */ @@ -1130,7 +1001,7 @@ static void add_jsonb(Datum val, bool is_null, JsonbInState *result, Oid val_type, bool key_scalar) { - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; if (val_type == InvalidOid) @@ -1140,12 +1011,12 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, if (is_null) { - tcategory = JSONBTYPE_NULL; + tcategory = JSONTYPE_NULL; outfuncoid = InvalidOid; } else - jsonb_categorize_type(val_type, - &tcategory, &outfuncoid); + json_categorize_type(val_type, true, + &tcategory, &outfuncoid); datum_to_jsonb(val, is_null, result, tcategory, outfuncoid, key_scalar); } @@ -1160,33 +1031,33 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, bool to_jsonb_is_immutable(Oid typoid) { - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; - jsonb_categorize_type(typoid, &tcategory, &outfuncoid); + json_categorize_type(typoid, true, &tcategory, &outfuncoid); switch (tcategory) { - case JSONBTYPE_NULL: - case JSONBTYPE_BOOL: - case JSONBTYPE_JSON: - case JSONBTYPE_JSONB: + case JSONTYPE_NULL: + case JSONTYPE_BOOL: + case JSONTYPE_JSON: + case JSONTYPE_JSONB: return true; - case JSONBTYPE_DATE: - case JSONBTYPE_TIMESTAMP: - case JSONBTYPE_TIMESTAMPTZ: + case JSONTYPE_DATE: + case JSONTYPE_TIMESTAMP: + case JSONTYPE_TIMESTAMPTZ: return false; - case JSONBTYPE_ARRAY: + case JSONTYPE_ARRAY: return false; /* TODO recurse into elements */ - case JSONBTYPE_COMPOSITE: + case JSONTYPE_COMPOSITE: return false; /* TODO recurse into fields */ - case JSONBTYPE_NUMERIC: - case JSONBTYPE_JSONCAST: - case JSONBTYPE_OTHER: + case JSONTYPE_NUMERIC: + case JSONTYPE_CAST: + case JSONTYPE_OTHER: return func_volatile(outfuncoid) == PROVOLATILE_IMMUTABLE; } @@ -1202,7 +1073,7 @@ to_jsonb(PG_FUNCTION_ARGS) Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); JsonbInState result; - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; if (val_type == InvalidOid) @@ -1210,8 +1081,8 @@ to_jsonb(PG_FUNCTION_ARGS) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - jsonb_categorize_type(val_type, - &tcategory, &outfuncoid); + json_categorize_type(val_type, true, + &tcategory, &outfuncoid); memset(&result, 0, sizeof(JsonbInState)); @@ -1636,8 +1507,8 @@ jsonb_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null) WJB_BEGIN_ARRAY, NULL); MemoryContextSwitchTo(oldcontext); - jsonb_categorize_type(arg_type, &state->val_category, - &state->val_output_func); + json_categorize_type(arg_type, true, &state->val_category, + &state->val_output_func); } else { @@ -1816,8 +1687,8 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - jsonb_categorize_type(arg_type, &state->key_category, - &state->key_output_func); + json_categorize_type(arg_type, true, &state->key_category, + &state->key_output_func); arg_type = get_fn_expr_argtype(fcinfo->flinfo, 2); @@ -1826,8 +1697,8 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - jsonb_categorize_type(arg_type, &state->val_category, - &state->val_output_func); + json_categorize_type(arg_type, true, &state->val_category, + &state->val_output_func); } else { diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index 70cb922e6b..612bbf06a3 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -26,6 +26,7 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" #include "nodes/miscnodes.h" +#include "parser/parse_coerce.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -5685,3 +5686,113 @@ json_get_first_token(text *json, bool throw_error) return JSON_TOKEN_INVALID; /* invalid json */ } + +/* + * Determine how we want to print values of a given type in datum_to_json(b). + * + * Given the datatype OID, return its JsonTypeCategory, as well as the type's + * output function OID. If the returned category is JSONTYPE_CAST or + * JSOBTYPE_CASTJSON, we return the OID of the type->JSON cast function + * instead. + */ +void +json_categorize_type(Oid typoid, bool is_jsonb, + JsonTypeCategory *tcategory, Oid *outfuncoid) +{ + bool typisvarlena; + + /* Look through any domain */ + typoid = getBaseType(typoid); + + *outfuncoid = InvalidOid; + + /* + * We need to get the output function for everything except date and + * timestamp types, booleans, array and composite types, json and jsonb, + * and non-builtin types where there's a cast to json. In this last case + * we return the oid of the cast function instead. + */ + + switch (typoid) + { + case BOOLOID: + *tcategory = JSONTYPE_BOOL; + break; + + case INT2OID: + case INT4OID: + case INT8OID: + case FLOAT4OID: + case FLOAT8OID: + case NUMERICOID: + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + *tcategory = JSONTYPE_NUMERIC; + break; + + case DATEOID: + *tcategory = JSONTYPE_DATE; + break; + + case TIMESTAMPOID: + *tcategory = JSONTYPE_TIMESTAMP; + break; + + case TIMESTAMPTZOID: + *tcategory = JSONTYPE_TIMESTAMPTZ; + break; + + case JSONOID: + if (!is_jsonb) + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + *tcategory = JSONTYPE_JSON; + break; + + case JSONBOID: + if (!is_jsonb) + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + *tcategory = is_jsonb ? JSONTYPE_JSONB : JSONTYPE_JSON; + break; + + default: + /* Check for arrays and composites */ + if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID + || typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID) + *tcategory = JSONTYPE_ARRAY; + else if (type_is_rowtype(typoid)) /* includes RECORDOID */ + *tcategory = JSONTYPE_COMPOSITE; + else + { + /* + * It's probably the general case. But let's look for a cast + * to json (note: not to jsonb even if is_jsonb is true), if + * it's not built-in. + */ + *tcategory = JSONTYPE_OTHER; + if (typoid >= FirstNormalObjectId) + { + Oid castfunc; + CoercionPathType ctype; + + ctype = find_coercion_pathway(JSONOID, typoid, + COERCION_EXPLICIT, + &castfunc); + if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc)) + { + *outfuncoid = castfunc; + *tcategory = JSONTYPE_CAST; + } + else + { + /* non builtin type with no cast */ + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + } + } + else + { + /* any other builtin type */ + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + } + } + break; + } +} diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index a85203d4a4..27c2d20610 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -63,4 +63,24 @@ extern Jsonb *transform_jsonb_string_values(Jsonb *jsonb, void *action_state, extern text *transform_json_string_values(text *json, void *action_state, JsonTransformStringValuesAction transform_action); +/* Type categories for datum_to_json[b] and friends. */ +typedef enum +{ + JSONTYPE_NULL, /* null, so we didn't bother to identify */ + JSONTYPE_BOOL, /* boolean (built-in types only) */ + JSONTYPE_NUMERIC, /* numeric (ditto) */ + JSONTYPE_DATE, /* we use special formatting for datetimes */ + JSONTYPE_TIMESTAMP, + JSONTYPE_TIMESTAMPTZ, + JSONTYPE_JSON, /* JSON and JSONB */ + JSONTYPE_JSONB, /* JSONB (for datum_to_jsonb) */ + JSONTYPE_ARRAY, /* array */ + JSONTYPE_COMPOSITE, /* composite */ + JSONTYPE_CAST, /* something with an explicit cast to JSON */ + JSONTYPE_OTHER, /* all else */ +} JsonTypeCategory; + +void json_categorize_type(Oid typoid, bool is_jsonb, + JsonTypeCategory *tcategory, Oid *outfuncoid); + #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e941fb6c82..b10590a252 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1318,7 +1318,6 @@ JsonbIteratorToken JsonbPair JsonbParseState JsonbSubWorkspace -JsonbTypeCategory JsonbValue JumbleState JunkFilter -- 2.35.3
v6-0002-SQL-JSON-functions.patch (application/octet-stream, 54.0K) download From e96397ddab1f289f1dea782f620a12b0760d4949 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:12:39 +0900 Subject: [PATCH v6 2/5] SQL JSON functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This Patch introduces three SQL standard JSON functions: JSON() JSON_SCALAR() JSON_SERIALIZE() JSON() produces json values from text, bytea, json or jsonb values, and has facilitites for handling duplicate keys. JSON_SCALAR() produces a json value from any scalar sql value, including json and jsonb. JSON_SERIALIZE() produces text or bytea from input which containis or represents json or jsonb; For the most part these functions don't add any significant new capabilities, but they will be of use to users wanting standard compliant JSON handling. Author: Nikita Glukhov <n.gluhov@postgrespro.ru> Author: Teodor Sigaev <teodor@sigaev.ru> Author: Oleg Bartunov <obartunov@gmail.com> Author: Alexander Korotkov <aekorotkov@gmail.com> Author: Andrew Dunstan <andrew@dunslane.net> Author: Amit Langote <amitlangote09@gmail.com> Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- doc/src/sgml/func.sgml | 71 +++++ src/backend/executor/execExpr.c | 30 ++ src/backend/executor/execExprInterp.c | 43 ++- src/backend/nodes/nodeFuncs.c | 30 ++ src/backend/parser/gram.y | 69 ++++- src/backend/parser/parse_expr.c | 234 +++++++++++++++- src/backend/parser/parse_target.c | 9 + src/backend/utils/adt/format_type.c | 4 + src/backend/utils/adt/json.c | 21 +- src/backend/utils/adt/jsonb.c | 48 +++- src/backend/utils/adt/ruleutils.c | 14 +- src/include/nodes/parsenodes.h | 48 ++++ src/include/nodes/primnodes.h | 5 +- src/include/parser/kwlist.h | 4 +- src/include/utils/jsonb.h | 2 +- src/include/utils/jsonfuncs.h | 4 + src/test/regress/expected/sqljson.out | 384 ++++++++++++++++++++++++++ src/test/regress/sql/sqljson.sql | 95 +++++++ src/tools/pgindent/typedefs.list | 1 + 19 files changed, 1067 insertions(+), 49 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 0b62e0c828..9cece06c18 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16001,6 +16001,77 @@ table2-mapping <returnvalue>{"a": "1", "b": "2"}</returnvalue> </para></entry> </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json constructor</primary></indexterm> + <function>json</function> ( + <replaceable>expression</replaceable> + <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional></optional> + <optional> { <literal>WITH</literal> | <literal>WITHOUT</literal> } <literal>UNIQUE</literal> <optional> <literal>KEYS</literal> </optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>json_data_type</replaceable> </optional>) + </para> + <para> + The <replaceable>expression</replaceable> can be any text type or a + <type>bytea</type> in UTF8 encoding. If the + <replaceable>expression</replaceable> is NULL, an + <acronym>SQL</acronym> null value is returned. + If <literal>WITH UNIQUE</literal> is specified, the + <replaceable>expression</replaceable> must not contain any duplicate + object keys. + </para> + <para> + <literal>json('{"a":123, "b":[true,"foo"], "a":"bar"}')</literal> + <returnvalue>{"a":123, "b":[true,"foo"], "a":"bar"}</returnvalue> + </para> + <para> + <literal>json('{"a":123,"b":[true,"foo"],"a":"bar"}' returning jsonb)</literal> + <returnvalue>{"a": "bar", "b": [true, "foo"]}</returnvalue> + </para> + </entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json_scalar</primary></indexterm> + <function>json_scalar</function> (<replaceable>expression</replaceable>) + <optional> <literal>RETURNING</literal> <replaceable>json_data_type</replaceable> </optional>) + </para> + <para> + Returns a JSON scalar value representing + <replaceable>expression</replaceable>. + If the input is NULL, an SQL NULL is returned. If the input is a number + or a boolean value, a corresponding JSON number or boolean value is + returned. For any other value a JSON string is returned. + </para> + <para> + <literal>json_scalar(123.45)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_scalar(CURRENT_TIMESTAMP)</literal> + <returnvalue>"2022-05-10T10:51:04.62128-04:00"</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <function>json_serialize</function> ( + <replaceable>expression</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional>) + </para> + <para> + Transforms an SQL/JSON value into a character or binary string. The + <replaceable>expression</replaceable> can be of any JSON type, any + character string type, or <type>bytea</type> in UTF8 encoding. + The returned type can be any character string type or + <type>bytea</type>. The default is <type>text</type>. + </para> + <para> + <literal>json_serialize('{ "a" : 1 } ' RETURNING bytea)</literal> + <returnvalue>\x7b20226122203a2031207d20</returnvalue> + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index bf3a08c5f0..6ca4098bef 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -48,6 +48,7 @@ #include "utils/array.h" #include "utils/builtins.h" #include "utils/datum.h" +#include "utils/jsonfuncs.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -2311,6 +2312,12 @@ ExecInitExprRec(Expr *node, ExprState *state, { ExecInitExprRec(ctor->func, state, resv, resnull); } + else if ((ctor->type == JSCTOR_JSON_PARSE && !ctor->unique) || + ctor->type == JSCTOR_JSON_SERIALIZE) + { + /* Use the value of the first argument as a result */ + ExecInitExprRec(linitial(args), state, resv, resnull); + } else { JsonConstructorExprState *jcstate; @@ -2349,6 +2356,29 @@ ExecInitExprRec(Expr *node, ExprState *state, argno++; } + /* prepare type cache for datum_to_json[b]() */ + if (ctor->type == JSCTOR_JSON_SCALAR) + { + bool is_jsonb = + ctor->returning->format->format_type == JS_FORMAT_JSONB; + + jcstate->arg_type_cache = + palloc(sizeof(*jcstate->arg_type_cache) * nargs); + + for (int i = 0; i < nargs; i++) + { + JsonTypeCategory category; + Oid outfuncid; + Oid typid = jcstate->arg_types[i]; + + json_categorize_type(typid, is_jsonb, + &category, &outfuncid); + + jcstate->arg_type_cache[i].outfuncid = outfuncid; + jcstate->arg_type_cache[i].category = (int) category; + } + } + ExprEvalPushStep(state, &scratch); } diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 851946a927..76e59691e5 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -3992,7 +3992,7 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_values, jcstate->arg_nulls, jcstate->arg_types, - jcstate->constructor->absent_on_null); + ctor->absent_on_null); else if (ctor->type == JSCTOR_JSON_OBJECT) res = (is_jsonb ? jsonb_build_object_worker : @@ -4002,6 +4002,47 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_types, jcstate->constructor->absent_on_null, jcstate->constructor->unique); + else if (ctor->type == JSCTOR_JSON_SCALAR) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + Oid outfuncid = jcstate->arg_type_cache[0].outfuncid; + JsonTypeCategory category = (JsonTypeCategory) + jcstate->arg_type_cache[0].category; + + if (is_jsonb) + res = to_jsonb_worker(value, category, outfuncid); + else + res = to_json_worker(value, category, outfuncid); + } + } + else if (ctor->type == JSCTOR_JSON_PARSE) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + text *js = DatumGetTextP(value); + + if (is_jsonb) + res = jsonb_from_text(js, true); + else + { + (void) json_validate(js, true, true); + res = value; + } + } + } else elog(ERROR, "invalid JsonConstructorExpr type %d", ctor->type); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index c41e6bb984..dda964bd19 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -3901,6 +3901,36 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonParseExpr: + { + JsonParseExpr *jpe = (JsonParseExpr *) node; + + if (WALK(jpe->expr)) + return true; + if (WALK(jpe->output)) + return true; + } + break; + case T_JsonScalarExpr: + { + JsonScalarExpr *jse = (JsonScalarExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; + case T_JsonSerializeExpr: + { + JsonSerializeExpr *jse = (JsonSerializeExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; case T_JsonConstructorExpr: { JsonConstructorExpr *ctor = (JsonConstructorExpr *) node; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index edb6c00ece..341b002dc7 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -566,7 +566,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <list> copy_options %type <typnam> Typename SimpleTypename ConstTypename - GenericType Numeric opt_float + GenericType Numeric opt_float JsonType Character ConstCharacter CharacterWithLength CharacterWithoutLength ConstDatetime ConstInterval @@ -647,7 +647,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> json_format_clause_opt json_value_expr - json_output_clause_opt + json_returning_clause_opt json_name_and_value json_aggregate_func %type <list> json_name_and_value_list @@ -659,7 +659,6 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt - /* * Non-keyword token types. These are hard-wired into the "flex" lexer. * They must be listed first so that their numeric codes do not depend on @@ -723,6 +722,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG + JSON_SCALAR JSON_SERIALIZE KEY KEYS @@ -13981,6 +13981,7 @@ SimpleTypename: $$->typmods = list_make2(makeIntConst(INTERVAL_FULL_RANGE, -1), makeIntConst($3, @3)); } + | JsonType { $$ = $1; } ; /* We have a separate ConstTypename to allow defaulting fixed-length @@ -13999,6 +14000,7 @@ ConstTypename: | ConstBit { $$ = $1; } | ConstCharacter { $$ = $1; } | ConstDatetime { $$ = $1; } + | JsonType { $$ = $1; } ; /* @@ -14367,6 +14369,13 @@ interval_second: } ; +JsonType: + JSON + { + $$ = SystemTypeName("json"); + $$->location = @1; + } + ; /***************************************************************************** * @@ -15561,7 +15570,7 @@ func_expr_common_subexpr: | JSON_OBJECT '(' json_name_and_value_list json_object_constructor_null_clause_opt json_key_uniqueness_constraint_opt - json_output_clause_opt ')' + json_returning_clause_opt ')' { JsonObjectConstructor *n = makeNode(JsonObjectConstructor); @@ -15572,7 +15581,7 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } - | JSON_OBJECT '(' json_output_clause_opt ')' + | JSON_OBJECT '(' json_returning_clause_opt ')' { JsonObjectConstructor *n = makeNode(JsonObjectConstructor); @@ -15586,7 +15595,7 @@ func_expr_common_subexpr: | JSON_ARRAY '(' json_value_expr_list json_array_constructor_null_clause_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayConstructor *n = makeNode(JsonArrayConstructor); @@ -15601,7 +15610,7 @@ func_expr_common_subexpr: select_no_parens json_format_clause_opt /* json_array_constructor_null_clause_opt */ - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayQueryConstructor *n = makeNode(JsonArrayQueryConstructor); @@ -15614,7 +15623,7 @@ func_expr_common_subexpr: $$ = (Node *) n; } | JSON_ARRAY '(' - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayConstructor *n = makeNode(JsonArrayConstructor); @@ -15625,7 +15634,37 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } - ; + | JSON '(' json_value_expr json_key_uniqueness_constraint_opt + json_returning_clause_opt ')' + { + JsonParseExpr *n = makeNode(JsonParseExpr); + + n->expr = (JsonValueExpr *) $3; + n->unique_keys = $4; + n->output = (JsonOutput *) $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SCALAR '(' a_expr json_returning_clause_opt ')' + { + JsonScalarExpr *n = makeNode(JsonScalarExpr); + + n->expr = (Expr *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SERIALIZE '(' json_value_expr json_returning_clause_opt ')' + { + JsonSerializeExpr *n = makeNode(JsonSerializeExpr); + + n->expr = (JsonValueExpr *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + ; + /* * SQL/XML support @@ -16373,7 +16412,7 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; -json_output_clause_opt: +json_returning_clause_opt: RETURNING Typename json_format_clause_opt { JsonOutput *n = makeNode(JsonOutput); @@ -16446,7 +16485,7 @@ json_aggregate_func: json_name_and_value json_object_constructor_null_clause_opt json_key_uniqueness_constraint_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonObjectAgg *n = makeNode(JsonObjectAgg); @@ -16464,7 +16503,7 @@ json_aggregate_func: json_value_expr json_array_aggregate_order_by_clause_opt json_array_constructor_null_clause_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayAgg *n = makeNode(JsonArrayAgg); @@ -17064,7 +17103,6 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION - | JSON | KEY | KEYS | LABEL @@ -17279,10 +17317,13 @@ col_name_keyword: | INT_P | INTEGER | INTERVAL + | JSON | JSON_ARRAY | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | LEAST | NATIONAL | NCHAR @@ -17643,6 +17684,8 @@ bare_label_keyword: | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | KEY | KEYS | LABEL diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 26344743e4..b30094af84 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -86,6 +86,10 @@ static Node *transformJsonArrayQueryConstructor(ParseState *pstate, static Node *transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg); static Node *transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg); static Node *transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred); +static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); +static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); +static Node *transformJsonSerializeExpr(ParseState *pstate, + JsonSerializeExpr * expr); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -337,6 +341,18 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonIsPredicate(pstate, (JsonIsPredicate *) expr); break; + case T_JsonParseExpr: + result = transformJsonParseExpr(pstate, (JsonParseExpr *) expr); + break; + + case T_JsonScalarExpr: + result = transformJsonScalarExpr(pstate, (JsonScalarExpr *) expr); + break; + + case T_JsonSerializeExpr: + result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3208,7 +3224,8 @@ makeJsonByteaToTextConversion(Node *expr, JsonFormat *format, int location) */ static Node * transformJsonValueExpr(ParseState *pstate, char *constructName, - JsonValueExpr *ve, JsonFormatType default_format) + JsonValueExpr *ve, JsonFormatType default_format, + Oid targettype) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3250,12 +3267,14 @@ transformJsonValueExpr(ParseState *pstate, char *constructName, else format = default_format; - if (format != JS_FORMAT_DEFAULT) + if (format != JS_FORMAT_DEFAULT || + (OidIsValid(targettype) && exprtype != targettype)) { - Oid targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; Node *coerced; + bool cast_is_needed = OidIsValid(targettype); - if (exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) + if (!cast_is_needed && + exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), errmsg(ve->format->format_type == JS_FORMAT_DEFAULT ? @@ -3271,6 +3290,9 @@ transformJsonValueExpr(ParseState *pstate, char *constructName, exprtype = TEXTOID; } + if (!OidIsValid(targettype)) + targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; + /* Try to coerce to the target type. */ coerced = coerce_to_target_type(pstate, expr, exprtype, targettype, -1, @@ -3281,11 +3303,20 @@ transformJsonValueExpr(ParseState *pstate, char *constructName, if (!coerced) { /* If coercion failed, use to_json()/to_jsonb() functions. */ - Oid fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; - FuncExpr *fexpr = makeFuncExpr(fnoid, targettype, - list_make1(expr), - InvalidOid, InvalidOid, - COERCE_EXPLICIT_CALL); + FuncExpr *fexpr; + Oid fnoid; + + if (cast_is_needed) /* only CAST is allowed */ + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(exprtype), + format_type_be(targettype)), + parser_errposition(pstate, location))); + + fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; + fexpr = makeFuncExpr(fnoid, targettype, list_make1(expr), + InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL); fexpr->location = location; @@ -3582,7 +3613,8 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) Node *key = transformExprRecurse(pstate, (Node *) kv->key); Node *val = transformJsonValueExpr(pstate, "JSON_OBJECT()", kv->value, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, key); args = lappend(args, val); @@ -3766,9 +3798,8 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) Oid aggtype; key = transformExprRecurse(pstate, (Node *) agg->arg->key); - val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", - agg->arg->value, - JS_FORMAT_DEFAULT); + val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", agg->arg->value, + JS_FORMAT_DEFAULT, InvalidOid); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3826,7 +3857,7 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", agg->arg, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, InvalidOid); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3874,7 +3905,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, "JSON_ARRAY()", jsval, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, val); } @@ -3957,3 +3989,175 @@ transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred) return makeJsonIsPredicate(expr, NULL, pred->item_type, pred->unique_keys, pred->location); } + +/* + * Transform the output clause of a JSON_*() expression if there is one and + * create one if not. + */ +static JsonReturning * +transformJsonReturning(ParseState *pstate, JsonOutput *output, const char *fname) +{ + JsonReturning *returning; + + if (output) + { + returning = transformJsonOutput(pstate, output, false); + + Assert(OidIsValid(returning->typid)); + + if (returning->typid != JSONOID && returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), fname), + parser_errposition(pstate, output->typeName->location))); + } + else + { + /* Output type is JSON by default. */ + Oid targettype = JSONOID; + JsonFormatType format = JS_FORMAT_JSON; + + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(format, JS_ENC_DEFAULT, -1); + returning->typid = targettype; + returning->typmod = -1; + } + + return returning; +} + +/* + * Transform a JSON() expression. + */ +static Node * +transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) +{ + JsonOutput *output = jsexpr->output; + JsonReturning *returning; + Node *arg; + + /* Disallow FORMAT specification in the RETURNING clause. */ + if (output) + { + JsonFormat *format = output->returning->format; + + if(format->format_type != JS_FORMAT_DEFAULT || + format->encoding != JS_ENC_DEFAULT) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot specify FORMAT in RETURNING clause of JSON()"), + parser_errposition(pstate, format->location))); + } + + returning = transformJsonReturning(pstate, output, "JSON()"); + + if (jsexpr->unique_keys) + { + /* + * Coerce string argument to text and then to json[b] in the executor + * node with key uniqueness check. + */ + JsonValueExpr *jve = jsexpr->expr; + Oid arg_type; + + arg = transformJsonParseArg(pstate, (Node *) jve->raw_expr, jve->format, + &arg_type); + + if (arg_type != TEXTOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use non-string types with WITH UNIQUE KEYS clause"), + parser_errposition(pstate, jsexpr->location))); + } + else + { + /* + * Coerce argument to target type using CAST for compatibility with PG + * function-like CASTs. + */ + arg = transformJsonValueExpr(pstate, "JSON()", jsexpr->expr, + JS_FORMAT_JSON, returning->typid); + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, + returning, jsexpr->unique_keys, false, + jsexpr->location); +} + +/* + * Transform a JSON_SCALAR() expression. + */ +static Node * +transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *jsexpr) +{ + Node *arg = transformExprRecurse(pstate, (Node *) jsexpr->expr); + JsonOutput *output = jsexpr->output; + JsonReturning *returning; + + /* Disallow FORMAT specification in the RETURNING clause. */ + if (output) + { + JsonFormat *format = output->returning->format; + + if(format->format_type != JS_FORMAT_DEFAULT || + format->encoding != JS_ENC_DEFAULT) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot specify FORMAT in RETURNING clause of JSON_SCALAR()"), + parser_errposition(pstate, format->location))); + } + + returning = transformJsonReturning(pstate, output, "JSON_SCALAR()"); + + if (exprType(arg) == UNKNOWNOID) + arg = coerce_to_specific_type(pstate, arg, TEXTOID, "JSON_SCALAR"); + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SCALAR, list_make1(arg), NULL, + returning, false, false, jsexpr->location); +} + +/* + * Transform a JSON_SERIALIZE() expression. + */ +static Node * +transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) +{ + Node *arg = transformJsonValueExpr(pstate, "JSON_SERIALIZE()", + expr->expr, + JS_FORMAT_JSON, + InvalidOid); + JsonReturning *returning; + + if (expr->output) + { + returning = transformJsonOutput(pstate, expr->output, true); + + if (returning->typid != BYTEAOID) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(returning->typid, &typcategory, + &typispreferred); + if (typcategory != TYPCATEGORY_STRING) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), + "JSON_SERIALIZE()"), + errhint("Try returning a string type or bytea."))); + } + } + else + { + /* RETURNING TEXT FORMAT JSON is by default */ + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(JS_FORMAT_JSON, JS_ENC_DEFAULT, -1); + returning->typid = TEXTOID; + returning->typmod = -1; + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), + NULL, returning, false, false, expr->location); +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4cca97ff9c..520d4f2a23 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1953,6 +1953,15 @@ FigureColnameInternal(Node *node, char **name) /* make XMLSERIALIZE act like a regular function */ *name = "xmlserialize"; return 2; + case T_JsonParseExpr: + *name = "json"; + return 2; + case T_JsonScalarExpr: + *name = "json_scalar"; + return 2; + case T_JsonSerializeExpr: + *name = "json_serialize"; + return 2; case T_JsonObjectConstructor: /* make JSON_OBJECT act like a regular function */ *name = "json_object"; diff --git a/src/backend/utils/adt/format_type.c b/src/backend/utils/adt/format_type.c index 12402a0637..36c45a3978 100644 --- a/src/backend/utils/adt/format_type.c +++ b/src/backend/utils/adt/format_type.c @@ -294,6 +294,10 @@ format_type_extended(Oid type_oid, int32 typemod, bits16 flags) else buf = pstrdup("character varying"); break; + + case JSONOID: + buf = pstrdup("json"); + break; } if (buf == NULL) diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index f6bef9c148..c316f848e1 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -653,6 +653,20 @@ row_to_json_pretty(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); } +/* + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. + */ +Datum +to_json_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid) +{ + StringInfo result = makeStringInfo(); + + datum_to_json(val, false, result, tcategory, outfuncoid, false); + + return PointerGetDatum(cstring_to_text_with_len(result->data, result->len)); +} + /* * Is the given type immutable when coming out of a JSON context? * @@ -704,7 +718,6 @@ to_json(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - StringInfo result; JsonTypeCategory tcategory; Oid outfuncoid; @@ -716,11 +729,7 @@ to_json(PG_FUNCTION_ARGS) json_categorize_type(val_type, false, &tcategory, &outfuncoid); - result = makeStringInfo(); - - datum_to_json(val, false, result, tcategory, outfuncoid, false); - - PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); + PG_RETURN_DATUM(to_json_worker(val, tcategory, outfuncoid)); } /* diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index fc64f56868..06ba409e64 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -33,6 +33,7 @@ typedef struct JsonbInState { JsonbParseState *parseState; JsonbValue *res; + bool unique_keys; Node *escontext; } JsonbInState; @@ -45,7 +46,8 @@ typedef struct JsonbAggState Oid val_output_func; } JsonbAggState; -static inline Datum jsonb_from_cstring(char *json, int len, Node *escontext); +static inline Datum jsonb_from_cstring(char *json, int len, bool unique_keys, + Node *escontext); static bool checkStringLen(size_t len, Node *escontext); static JsonParseErrorType jsonb_in_object_start(void *pstate); static JsonParseErrorType jsonb_in_object_end(void *pstate); @@ -76,7 +78,7 @@ jsonb_in(PG_FUNCTION_ARGS) { char *json = PG_GETARG_CSTRING(0); - return jsonb_from_cstring(json, strlen(json), fcinfo->context); + return jsonb_from_cstring(json, strlen(json), false, fcinfo->context); } /* @@ -100,7 +102,7 @@ jsonb_recv(PG_FUNCTION_ARGS) else elog(ERROR, "unsupported jsonb version number %d", version); - return jsonb_from_cstring(str, nbytes, NULL); + return jsonb_from_cstring(str, nbytes, false, NULL); } /* @@ -141,6 +143,18 @@ jsonb_send(PG_FUNCTION_ARGS) PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); } +/* + * Note: exported for use in SQL/JSON executor functions. + */ +Datum +jsonb_from_text(text *js, bool unique_keys) +{ + return jsonb_from_cstring(VARDATA_ANY(js), + VARSIZE_ANY_EXHDR(js), + unique_keys, + NULL); +} + /* * Get the type name of a jsonb container. */ @@ -234,7 +248,7 @@ jsonb_typeof(PG_FUNCTION_ARGS) * instead of being thrown. */ static inline Datum -jsonb_from_cstring(char *json, int len, Node *escontext) +jsonb_from_cstring(char *json, int len, bool unique_keys, Node *escontext) { JsonLexContext *lex; JsonbInState state; @@ -244,6 +258,7 @@ jsonb_from_cstring(char *json, int len, Node *escontext) memset(&sem, 0, sizeof(sem)); lex = makeJsonLexContextCstringLen(json, len, GetDatabaseEncoding(), true); + state.unique_keys = unique_keys; state.escontext = escontext; sem.semstate = (void *) &state; @@ -280,6 +295,7 @@ jsonb_in_object_start(void *pstate) JsonbInState *_state = (JsonbInState *) pstate; _state->res = pushJsonbValue(&_state->parseState, WJB_BEGIN_OBJECT, NULL); + _state->parseState->unique_keys = _state->unique_keys; return JSON_SUCCESS; } @@ -1021,6 +1037,23 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, datum_to_jsonb(val, is_null, result, tcategory, outfuncoid, key_scalar); } + +/* + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. + */ +Datum +to_jsonb_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid) +{ + JsonbInState result; + + memset(&result, 0, sizeof(JsonbInState)); + + datum_to_jsonb(val, false, &result, tcategory, outfuncoid, false); + + return JsonbPGetDatum(JsonbValueToJsonb(result.res)); +} + /* * Is the given type immutable when coming out of a JSONB context? * @@ -1072,7 +1105,6 @@ to_jsonb(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - JsonbInState result; JsonTypeCategory tcategory; Oid outfuncoid; @@ -1084,11 +1116,7 @@ to_jsonb(PG_FUNCTION_ARGS) json_categorize_type(val_type, true, &tcategory, &outfuncoid); - memset(&result, 0, sizeof(JsonbInState)); - - datum_to_jsonb(val, false, &result, tcategory, outfuncoid, false); - - PG_RETURN_POINTER(JsonbValueToJsonb(result.res)); + PG_RETURN_DATUM(to_jsonb_worker(val, tcategory, outfuncoid)); } Datum diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d3a973d86b..d1b03d6cb2 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -10832,6 +10832,15 @@ get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, case JSCTOR_JSON_ARRAY: funcname = "JSON_ARRAY"; break; + case JSCTOR_JSON_PARSE: + funcname = "JSON"; + break; + case JSCTOR_JSON_SCALAR: + funcname = "JSON_SCALAR"; + break; + case JSCTOR_JSON_SERIALIZE: + funcname = "JSON_SERIALIZE"; + break; default: elog(ERROR, "invalid JsonConstructorType %d", ctor->type); } @@ -10879,7 +10888,10 @@ get_json_constructor_options(JsonConstructorExpr *ctor, StringInfo buf) if (ctor->unique) appendStringInfoString(buf, " WITH UNIQUE KEYS"); - get_json_returning(ctor->returning, buf, true); + if (!((ctor->type == JSCTOR_JSON_PARSE || + ctor->type == JSCTOR_JSON_SCALAR) && + ctor->returning->typid == JSONOID)) + get_json_returning(ctor->returning, buf, true); } /* diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index efb5c3e098..d926713bd9 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1716,6 +1716,17 @@ typedef struct TriggerTransition /* Nodes for SQL/JSON support */ +/* + * JsonQuotes - + * representation of [KEEP|OMIT] QUOTES clause for JSON_QUERY() + */ +typedef enum JsonQuotes +{ + JS_QUOTES_UNSPEC, /* unspecified */ + JS_QUOTES_KEEP, /* KEEP QUOTES */ + JS_QUOTES_OMIT /* OMIT QUOTES */ +} JsonQuotes; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1739,6 +1750,43 @@ typedef struct JsonKeyValue JsonValueExpr *value; /* JSON value expression */ } JsonKeyValue; +/* + * JsonParseExpr - + * untransformed representation of JSON() + */ +typedef struct JsonParseExpr +{ + NodeTag type; + JsonValueExpr *expr; /* string expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + bool unique_keys; /* WITH UNIQUE KEYS? */ + int location; /* token location, or -1 if unknown */ +} JsonParseExpr; + +/* + * JsonScalarExpr - + * untransformed representation of JSON_SCALAR() + */ +typedef struct JsonScalarExpr +{ + NodeTag type; + Expr *expr; /* scalar expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonScalarExpr; + +/* + * JsonSerializeExpr - + * untransformed representation of JSON_SERIALIZE() function + */ +typedef struct JsonSerializeExpr +{ + NodeTag type; + JsonValueExpr *expr; /* json value expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonSerializeExpr; + /* * JsonObjectConstructor - * untransformed representation of JSON_OBJECT() constructor diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 0d2df069b3..85e484bf43 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1612,7 +1612,10 @@ typedef enum JsonConstructorType JSCTOR_JSON_OBJECT = 1, JSCTOR_JSON_ARRAY = 2, JSCTOR_JSON_OBJECTAGG = 3, - JSCTOR_JSON_ARRAYAGG = 4 + JSCTOR_JSON_ARRAYAGG = 4, + JSCTOR_JSON_SCALAR = 5, + JSCTOR_JSON_SERIALIZE = 6, + JSCTOR_JSON_PARSE = 7 } JsonConstructorType; /* diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index f5b2e61ca5..5984dcfa4b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -230,11 +230,13 @@ PG_KEYWORD("is", IS, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("isnull", ISNULL, TYPE_FUNC_NAME_KEYWORD, AS_LABEL) PG_KEYWORD("isolation", ISOLATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) -PG_KEYWORD("json", JSON, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index 649a1644f2..f928e6142a 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -368,7 +368,6 @@ typedef struct JsonbIterator struct JsonbIterator *parent; } JsonbIterator; - /* Convenience macros */ static inline Jsonb * DatumGetJsonbP(Datum d) @@ -418,6 +417,7 @@ extern void JsonbHashScalarValueExtended(const JsonbValue *scalarVal, uint64 *hash, uint64 seed); /* jsonb.c support functions */ +extern Datum jsonb_from_text(text *js, bool unique_keys); extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index 27c2d20610..586d948c94 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -82,5 +82,9 @@ typedef enum void json_categorize_type(Oid typoid, bool is_jsonb, JsonTypeCategory *tcategory, Oid *outfuncoid); +extern Datum to_json_worker(Datum val, JsonTypeCategory tcategory, + Oid outfuncoid); +extern Datum to_jsonb_worker(Datum val, JsonTypeCategory tcategory, + Oid outfuncoid); #endif diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index d73c7e2c6c..ddea8a072f 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -1,3 +1,345 @@ +-- JSON() +SELECT JSON(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON(); + ^ +SELECT JSON(NULL); + json +------ + +(1 row) + +SELECT JSON('{ "a" : 1 } '); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +ERROR: JSON ENCODING clause is only allowed for bytea input type +LINE 1: SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); + ^ +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + pg_typeof +----------- + json +(1 row) + +SELECT JSON(' 1 '::json); + json +--------- + 1 +(1 row) + +SELECT JSON(' 1 '::jsonb); + json +------ + 1 +(1 row) + +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +ERROR: cannot use non-string types with WITH UNIQUE KEYS clause +LINE 1: SELECT JSON(' 1 '::json WITH UNIQUE KEYS); + ^ +SELECT JSON(123); +ERROR: cannot cast type integer to json +LINE 1: SELECT JSON(123); + ^ +SELECT JSON('{"a": 1, "a": 2}'); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +ERROR: duplicate JSON object key value +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); + QUERY PLAN +----------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); + QUERY PLAN +------------------------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON ENCODING UTF8) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); + QUERY PLAN +---------------------------------------------- + Result + Output: JSON('123'::text WITH UNIQUE KEYS) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +SELECT JSON('123' RETURNING text); +ERROR: cannot use RETURNING type text in JSON() +LINE 1: SELECT JSON('123' RETURNING text); + ^ +SELECT JSON('123' RETURNING text FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON() +LINE 1: SELECT JSON('123' RETURNING text FORMAT JSON); + ^ +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING json); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING jsonb); + QUERY PLAN +---------------------------------------------- + Result + Output: JSON('123'::jsonb RETURNING jsonb) +(2 rows) + +SELECT pg_typeof(JSON('123')); + pg_typeof +----------- + json +(1 row) + +SELECT pg_typeof(JSON('123' RETURNING json)); + pg_typeof +----------- + json +(1 row) + +SELECT pg_typeof(JSON('123' RETURNING jsonb)); + pg_typeof +----------- + jsonb +(1 row) + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SCALAR(); + ^ +SELECT JSON_SCALAR(NULL); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(NULL::int); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(123); + json_scalar +------------- + 123 +(1 row) + +SELECT JSON_SCALAR(123.45); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(123.45::numeric); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(true); + json_scalar +------------- + true +(1 row) + +SELECT JSON_SCALAR(false); + json_scalar +------------- + false +(1 row) + +SELECT JSON_SCALAR(' 123.45'); + json_scalar +------------- + " 123.45" +(1 row) + +SELECT JSON_SCALAR('2020-06-07'::date); + json_scalar +-------------- + "2020-06-07" +(1 row) + +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); + json_scalar +----------------------- + "2020-06-07T01:02:03" +(1 row) + +SELECT JSON_SCALAR('{}'::json); + json_scalar +------------- + {} +(1 row) + +SELECT JSON_SCALAR('{}'::jsonb); + json_scalar +------------- + {} +(1 row) + +SELECT JSON_SCALAR(123 RETURNING jsonb FORMAT JSON ENCODING UTF8); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_SCALAR() +LINE 1: SELECT JSON_SCALAR(123 RETURNING jsonb FORMAT JSON ENCODING ... + ^ +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); + QUERY PLAN +---------------------------- + Result + Output: JSON_SCALAR(123) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); + QUERY PLAN +------------------------------------ + Result + Output: JSON_SCALAR('123'::text) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING json); + QUERY PLAN +---------------------------- + Result + Output: JSON_SCALAR(123) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING jsonb); + QUERY PLAN +-------------------------------------------- + Result + Output: JSON_SCALAR(123 RETURNING jsonb) +(2 rows) + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SERIALIZE(); + ^ +SELECT JSON_SERIALIZE(NULL); + json_serialize +---------------- + +(1 row) + +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } '); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('1'); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('1' FORMAT JSON); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); + json_serialize +---------------------------- + \x7b20226122203a2031207d20 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + pg_typeof +----------- + text +(1 row) + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); +ERROR: cannot use RETURNING type jsonb in JSON_SERIALIZE() +HINT: Try returning a string type or bytea. +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); + QUERY PLAN +----------------------------------------------------- + Result + Output: JSON_SERIALIZE('{}'::json RETURNING text) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + QUERY PLAN +------------------------------------------------------ + Result + Output: JSON_SERIALIZE('{}'::json RETURNING bytea) +(2 rows) + -- JSON_OBJECT() SELECT JSON_OBJECT(); json_object @@ -630,6 +972,13 @@ ERROR: duplicate JSON object key SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); ERROR: duplicate JSON object key +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + json_objectagg +------------------ + {"1": 1, "2": 2} +(1 row) + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -645,6 +994,41 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); CREATE OR REPLACE VIEW public.json_object_view AS SELECT JSON_OBJECT('foo' : '1'::text FORMAT JSON, 'bar' : 'baz'::text RETURNING json) AS "json_object" DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + a | json_objectagg +---------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(2 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":1,"v":null} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1, "2" : 2 } + {"k":1,"v":null} | { "1" : 1, "2" : 2 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 4fd820fd51..f9afae0d42 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -1,3 +1,77 @@ +-- JSON() +SELECT JSON(); +SELECT JSON(NULL); +SELECT JSON('{ "a" : 1 } '); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + +SELECT JSON(' 1 '::json); +SELECT JSON(' 1 '::jsonb); +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +SELECT JSON(123); + +SELECT JSON('{"a": 1, "a": 2}'); +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + +SELECT JSON('123' RETURNING text); +SELECT JSON('123' RETURNING text FORMAT JSON); -- RETURNING FORMAT not allowed + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING json); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' RETURNING jsonb); +SELECT pg_typeof(JSON('123')); +SELECT pg_typeof(JSON('123' RETURNING json)); +SELECT pg_typeof(JSON('123' RETURNING jsonb)); + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +SELECT JSON_SCALAR(NULL); +SELECT JSON_SCALAR(NULL::int); +SELECT JSON_SCALAR(123); +SELECT JSON_SCALAR(123.45); +SELECT JSON_SCALAR(123.45::numeric); +SELECT JSON_SCALAR(true); +SELECT JSON_SCALAR(false); +SELECT JSON_SCALAR(' 123.45'); +SELECT JSON_SCALAR('2020-06-07'::date); +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); +SELECT JSON_SCALAR('{}'::json); +SELECT JSON_SCALAR('{}'::jsonb); +SELECT JSON_SCALAR(123 RETURNING jsonb FORMAT JSON ENCODING UTF8); -- RETURNING FORMAT not allowed + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING json); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123 RETURNING jsonb); + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +SELECT JSON_SERIALIZE(NULL); +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); +SELECT JSON_SERIALIZE('{ "a" : 1 } '); +SELECT JSON_SERIALIZE('1'); +SELECT JSON_SERIALIZE('1' FORMAT JSON); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); + + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + -- JSON_OBJECT() SELECT JSON_OBJECT(); SELECT JSON_OBJECT(RETURNING json); @@ -216,6 +290,9 @@ FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -227,6 +304,24 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index b10590a252..d251fb9a91 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1296,6 +1296,7 @@ JsonPathPredicateCallback JsonPathString JsonReturning JsonSemAction +JsonSerializeExpr JsonTokenType JsonTransformStringValuesAction JsonTypeCategory -- 2.35.3
v6-0005-Claim-SQL-standard-compliance-for-SQL-JSON-featur.patch (application/octet-stream, 2.3K) download From c498d63c5678f27b5d7477425097ff15242f1f3a Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:16:39 +0900 Subject: [PATCH v6 5/5] Claim SQL standard compliance for SQL/JSON features Discussion: https://postgr.es/m/d03d809c-d0fb-fd6a-1476-d6dc18ec940e@dunslane.net --- src/backend/catalog/sql_features.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt index b33065d7bf..b379c71df9 100644 --- a/src/backend/catalog/sql_features.txt +++ b/src/backend/catalog/sql_features.txt @@ -548,15 +548,15 @@ T811 Basic SQL/JSON constructor functions YES T812 SQL/JSON: JSON_OBJECTAGG YES T813 SQL/JSON: JSON_ARRAYAGG with ORDER BY YES T814 Colon in JSON_OBJECT or JSON_OBJECTAGG YES -T821 Basic SQL/JSON query operators NO +T821 Basic SQL/JSON query operators YES T822 SQL/JSON: IS JSON WITH UNIQUE KEYS predicate YES -T823 SQL/JSON: PASSING clause NO -T824 JSON_TABLE: specific PLAN clause NO -T825 SQL/JSON: ON EMPTY and ON ERROR clauses NO -T826 General value expression in ON ERROR or ON EMPTY clauses NO -T827 JSON_TABLE: sibling NESTED COLUMNS clauses NO -T828 JSON_QUERY NO -T829 JSON_QUERY: array wrapper options NO +T823 SQL/JSON: PASSING clause YES +T824 JSON_TABLE: specific PLAN clause YES +T825 SQL/JSON: ON EMPTY and ON ERROR clauses YES +T826 General value expression in ON ERROR or ON EMPTY clauses YES +T827 JSON_TABLE: sibling NESTED COLUMNS clauses YES +T828 JSON_QUERY YES +T829 JSON_QUERY: array wrapper options YES T830 Enforcing unique keys in SQL/JSON constructor functions YES T831 SQL/JSON path language: strict mode YES T832 SQL/JSON path language: item method YES @@ -565,7 +565,7 @@ T834 SQL/JSON path language: wildcard member accessor YES T835 SQL/JSON path language: filter expressions YES T836 SQL/JSON path language: starts with predicate YES T837 SQL/JSON path language: regex_like predicate YES -T838 JSON_TABLE: PLAN DEFAULT clause NO +T838 JSON_TABLE: PLAN DEFAULT clause YES T839 Formatted cast of datetimes to/from character strings NO T840 Hex integer literals in SQL/JSON path language YES T851 SQL/JSON: optional keywords for default syntax YES -- 2.35.3
v6-0003-SQL-JSON-query-functions.patch (application/octet-stream, 209.0K) download From b1d3cf3cb69ec1666d26ee7b99a38117b9727785 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Fri, 16 Jun 2023 17:49:18 +0900 Subject: [PATCH v6 3/5] SQL/JSON query functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This introduces the SQL/JSON functions for querying JSON data using jsonpath expressions. The functions are: JSON_EXISTS() JSON_QUERY() JSON_VALUE() All of these functions only operate on jsonb. The workaround for now is to cast the argument to jsonb. JSON_EXISTS() tests if the jsonpath expression applied to the jsonb value yields any values. JSON_VALUE() must return a single value, and an error occurs if it tries to return multiple values. JSON_QUERY() must return a json object or array, and there are various WRAPPER options for handling scalar or multi-value results. Both these functions have options for handling EMPTY and ERROR conditions. Author: Nikita Glukhov <n.gluhov@postgrespro.ru> Author: Teodor Sigaev <teodor@sigaev.ru> Author: Oleg Bartunov <obartunov@gmail.com> Author: Alexander Korotkov <aekorotkov@gmail.com> Author: Andrew Dunstan <andrew@dunslane.net> Author: Amit Langote <amitlangote09@gmail.com> Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- doc/src/sgml/func.sgml | 147 +++ src/backend/executor/execExpr.c | 432 ++++++++ src/backend/executor/execExprInterp.c | 502 ++++++++- src/backend/jit/llvm/llvmjit_expr.c | 250 +++++ src/backend/jit/llvm/llvmjit_types.c | 5 + src/backend/nodes/makefuncs.c | 15 + src/backend/nodes/nodeFuncs.c | 183 ++++ src/backend/optimizer/path/costsize.c | 3 +- src/backend/optimizer/util/clauses.c | 19 + src/backend/parser/gram.y | 348 ++++++- src/backend/parser/parse_collate.c | 7 + src/backend/parser/parse_expr.c | 519 ++++++++- src/backend/parser/parse_target.c | 15 + src/backend/utils/adt/formatting.c | 44 + src/backend/utils/adt/jsonb.c | 62 ++ src/backend/utils/adt/jsonfuncs.c | 170 ++- src/backend/utils/adt/jsonpath.c | 255 +++++ src/backend/utils/adt/jsonpath_exec.c | 391 ++++++- src/backend/utils/adt/ruleutils.c | 136 +++ src/include/executor/execExpr.h | 172 +++ src/include/executor/executor.h | 1 + src/include/nodes/execnodes.h | 3 + src/include/nodes/makefuncs.h | 1 + src/include/nodes/parsenodes.h | 48 + src/include/nodes/primnodes.h | 109 ++ src/include/parser/kwlist.h | 11 + src/include/utils/formatting.h | 2 +- src/include/utils/jsonb.h | 3 + src/include/utils/jsonfuncs.h | 5 + src/include/utils/jsonpath.h | 27 + src/interfaces/ecpg/preproc/ecpg.trailer | 28 + src/test/regress/expected/json_sqljson.out | 18 + src/test/regress/expected/jsonb_sqljson.out | 1042 +++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/json_sqljson.sql | 11 + src/test/regress/sql/jsonb_sqljson.sql | 327 ++++++ src/tools/pgindent/typedefs.list | 14 + 37 files changed, 5234 insertions(+), 93 deletions(-) create mode 100644 src/test/regress/expected/json_sqljson.out create mode 100644 src/test/regress/expected/jsonb_sqljson.out create mode 100644 src/test/regress/sql/json_sqljson.sql create mode 100644 src/test/regress/sql/jsonb_sqljson.sql diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 9cece06c18..cb36e1463b 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16995,6 +16995,153 @@ array w/o UK? | t </tbody> </tgroup> </table> + + <para> + <xref linkend="functions-sqljson-querying"/> details the SQL/JSON + functions that can be used to query JSON data. + </para> + + <note> + <para> + SQL/JSON paths can only be applied to the <type>jsonb</type> type, so it + might be necessary to cast the <replaceable>context_item</replaceable> + argument of these functions to <type>jsonb</type>. + </para> + </note> + + <table id="functions-sqljson-querying"> + <title>SQL/JSON Query Functions</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="func_table_entry"><para role="func_signature"> + Function signature + </para> + <para> + Description + </para> + <para> + Example(s) + </para></entry> + </row> + </thead> + <tbody> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_exists</primary></indexterm> + <function>json_exists</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>TRUE</literal> | <literal>FALSE</literal> |<literal> UNKNOWN</literal> | <literal>ERROR</literal> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns true if the SQL/JSON <replaceable>path_expression</replaceable> + applied to the <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s yields any items. + The <literal>ON ERROR</literal> clause specifies what is returned if + an error occurs. Note that if the <replaceable>path_expression</replaceable> + is <literal>strict</literal>, an error is generated if it yields no items. + The default value is <literal>UNKNOWN</literal> which causes a NULL + result. + </para> + <para> + <literal>json_exists(jsonb '{"key1": [1,2,3]}', 'strict $.key1[*] ? (@ > 2)')</literal> + <returnvalue>t</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'lax $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>f</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'strict $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>ERROR: jsonpath array subscript is out of bounds</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_value</primary></indexterm> + <function>json_value</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> + <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. The extracted value must be + a single <acronym>SQL/JSON</acronym> scalar item. For results that + are objects or arrays, use the <function>json_query</function> + function instead. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + The <literal>ON EMPTY</literal> clause specifies the behavior if the + <replaceable>path_expression</replaceable> yields no value at all. + The <literal>ON ERROR</literal> clause specifies the behavior if an + error occurs as a result of <type>jsonpath</type> evaluation + (including cast to the output type) or execution of + <literal>ON EMPTY</literal> behavior (that was caused by empty result + of <type>jsonpath</type> evaluation). + </para> + <para> + <literal>json_value(jsonb '"123.45"', '$' RETURNING float)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_value(jsonb '"03:04 2015-02-01"', '$.datetime("HH24:MI&nbsp;YYYY-MM-DD")' RETURNING date)</literal> + <returnvalue>2015-02-01</returnvalue> + </para> + <para> + <literal>json_value(jsonb '[1,2]', 'strict $[*]' DEFAULT 9 ON ERROR)</literal> + <returnvalue>9</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_query</primary></indexterm> + <function>json_query</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional> + <optional> { <literal>WITHOUT</literal> | <literal>WITH</literal> { <literal>CONDITIONAL</literal> | <optional><literal>UNCONDITIONAL</literal></optional> } } <optional> <literal>ARRAY</literal> </optional> <literal>WRAPPER</literal> </optional> + <optional> { <literal>KEEP</literal> | <literal>OMIT</literal> } <literal>QUOTES</literal> <optional> <literal>ON SCALAR STRING</literal> </optional> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. + This function must return a JSON string, so if the path expression + returns multiple SQL/JSON items, you must wrap the result using the + <literal>WITH WRAPPER</literal> clause. If the wrapper is + <literal>UNCONDITIONAL</literal>, an array wrapper will always + be applied, even if the returned value is already a single JSON object + or array, but if it is <literal>CONDITIONAL</literal>, it will not be + applied to a single array or object. <literal>UNCONDITIONAL</literal> + is the default. + If the result is a scalar string, by default the value returned will have + surrounding quotes making it a valid JSON value. However, this behavior + is reversed if <literal>OMIT QUOTES</literal> is specified. + The <literal>ON ERROR</literal> and <literal>ON EMPTY</literal> + clauses have similar semantics to those clauses for + <function>json_value</function>. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + </para> + <para> + <literal>json_query(jsonb '[1,[2,3],null]', 'lax $[*][1]' WITH CONDITIONAL WRAPPER)</literal> + <returnvalue>[3]</returnvalue> + </para></entry> + </row> + </tbody> + </tgroup> + </table> + </sect2> <sect2 id="functions-sqljson-path"> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 6ca4098bef..d3d2ce00d1 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -49,6 +49,7 @@ #include "utils/builtins.h" #include "utils/datum.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -88,6 +89,16 @@ static void ExecBuildAggTransCall(ExprState *state, AggState *aggstate, FunctionCallInfo fcinfo, AggStatePerTrans pertrans, int transno, int setno, int setoff, bool ishash, bool nullcheck); +static void ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch); +static Datum GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null); +static JsonCoercionState *ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull); +static JsonItemCoercionsState *ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull); /* @@ -2411,6 +2422,14 @@ ExecInitExprRec(Expr *node, ExprState *state, break; } + case T_JsonExpr: + { + JsonExpr *jexpr = castNode(JsonExpr, node); + + ExecInitJsonExpr(jexpr, state, resv, resnull, &scratch); + break; + } + case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -4178,3 +4197,416 @@ ExecBuildParamSetEqual(TupleDesc desc, return state; } + +/* + * Push steps to evaluate a JsonExpr and its various subsidiary expressions. + */ +static void +ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch) +{ + JsonExprState *jsestate = palloc0(sizeof(JsonExprState)); + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + ListCell *argexprlc; + ListCell *argnamelc; + int skip_step_off = -1; + int passing_args_step_off = -1; + int coercion_step_off = -1; + int coercion_finish_step_off = -1; + int behavior_step_off = -1; + int onempty_expr_step_off = -1; + int onempty_jump_step_off = -1; + int onerror_expr_step_off = -1; + int onerror_jump_step_off = -1; + List *adjust_jumps = NIL; + ListCell *lc; + ExprEvalStep *as; + + jsestate->jsexpr = jexpr; + + /* + * Add steps to compute formatted_expr, pathspec, and PASSING arg + * expressions as things that must be evaluated *before* the actual JSON + * path expression. + */ + ExecInitExprRec((Expr *) jexpr->formatted_expr, state, + &pre_eval->formatted_expr.value, + &pre_eval->formatted_expr.isnull); + ExecInitExprRec((Expr *) jexpr->path_spec, state, + &pre_eval->pathspec.value, + &pre_eval->pathspec.isnull); + + /* + * Before pushing steps for PASSING args, push a step to decide whether to + * skip evaluating the args and the JSON path expression depending on + * whether either of formatted_expr and pathspec is NULL; see + * ExecEvalJsonExprSkip(). + */ + scratch->opcode = EEOP_JSONEXPR_SKIP; + scratch->d.jsonexpr_skip.jsestate = jsestate; + skip_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* PASSING args. */ + jsestate->pre_eval.args = NIL; + passing_args_step_off = state->steps_len; + forboth(argexprlc, jexpr->passing_values, + argnamelc, jexpr->passing_names) + { + Expr *argexpr = (Expr *) lfirst(argexprlc); + String *argname = lfirst_node(String, argnamelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(argname->sval); + var->typid = exprType((Node *) argexpr); + var->typmod = exprTypmod((Node *) argexpr); + + ExecInitExprRec((Expr *) argexpr, state, &var->value, &var->isnull); + + pre_eval->args = lappend(pre_eval->args, var); + } + + /* + * Step for the actual JSON path evaluation; see ExecEvalJson() and + * ExecEvalJsonExpr(). + */ + scratch->opcode = EEOP_JSONEXPR_PATH; + scratch->d.jsonexpr.jsestate = jsestate; + ExprEvalPushStep(state, scratch); + + /* + * Push steps to control the evaluation of expressions based on the result + * of JSON path evaluation. + */ + + /* + * Step to handle ON ERROR and ON EMPTY behavior; see + * ExecEvalJsonExprBehavior(). + */ + scratch->opcode = EEOP_JSONEXPR_BEHAVIOR; + scratch->d.jsonexpr_behavior.jsestate = jsestate; + behavior_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Step(s) to evaluate ON EMPTY expression */ + onempty_expr_step_off = state->steps_len; + if (jexpr->on_empty && + jexpr->on_empty->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_empty->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_empty->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_empty, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onempty_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + } + } + + /* Step(s) to evaluate ON ERROR expression */ + onerror_expr_step_off = state->steps_len; + if (jexpr->on_error && + jexpr->on_error->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_error->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_error->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_error, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onerror_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set later */ + ExprEvalPushStep(state, scratch); + } + } + + /* + * Step to handle applying coercion to the JSON item returned by + * EEOP_JSONEXPR_PATH or to the ON EMPTY/ERROR expression as + * EEOP_JSONEXPR_BEHAVIOR decides; see ExecEvalJsonExprCoercion(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Initialize coercion expression(s). */ + if (jexpr->result_coercion) + { + jsestate->result_jcstate = + ExecInitJsonCoercion(scratch, state, jexpr->result_coercion, + resv, resnull); + /* See the comment above. */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* computed later */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + if (jexpr->coercions) + { + /* + * ExecPrepareJsonItemCoercion() called by ExecEvalJsonExpr() chooses + * one for a given JSON item returned by JsonPathValue(). + */ + jsestate->item_jcstates = + ExecInitJsonItemCoercions(scratch, state, jexpr->coercions, + resv, resnull); + } + + /* + * And a step to clean up after the coercion step; see + * ExecEvalJsonExprCoercionFinish(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION_FINISH; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_finish_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* + * Adjust jump target addresses in various post-eval steps now that we + * have all the steps in place. + */ + + /* EEOP_JSONEXPR_SKIP */ + Assert(skip_step_off >= 0); + as = &state->steps[skip_step_off]; + as->d.jsonexpr_skip.jump_coercion = coercion_step_off; + as->d.jsonexpr_skip.jump_passing_args = passing_args_step_off; + + /* EEOP_JSONEXPR_BEHAVIOR */ + Assert(behavior_step_off >= 0); + as = &state->steps[behavior_step_off]; + as->d.jsonexpr_behavior.jump_onerror_expr = onerror_expr_step_off; + as->d.jsonexpr_behavior.jump_onempty_expr = onempty_expr_step_off; + as->d.jsonexpr_behavior.jump_coercion = coercion_step_off; + as->d.jsonexpr_behavior.jump_skip_coercion = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION */ + Assert(coercion_step_off >= 0); + as = &state->steps[coercion_step_off]; + as->d.jsonexpr_coercion.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION_FINISH */ + Assert(coercion_finish_step_off >= 0); + as = &state->steps[coercion_finish_step_off]; + as->d.jsonexpr_coercion_finish.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion_finish.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JUMP steps */ + + /* + * Ones after ON EMPTY and ON ERROR non-default expressions should jump to + * the coercion step. + */ + if (onempty_jump_step_off >= 0) + { + as = &state->steps[onempty_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + if (onerror_jump_step_off >= 0) + { + as = &state->steps[onerror_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + + /* The rest should jump to the end. */ + foreach(lc, adjust_jumps) + { + as = &state->steps[lfirst_int(lc)]; + as->d.jump.jumpdone = state->steps_len; + } + + /* + * Set RETURNING type's input function used by ExecEvalJsonExprCoercion(). + */ + if (jexpr->omit_quotes || + (jexpr->result_coercion && jexpr->result_coercion->via_io)) + { + Oid typinput; + FmgrInfo *finfo; + + /* lookup the result type's input function */ + getTypeInputInfo(jexpr->returning->typid, &typinput, + &jsestate->input.typioparam); + finfo = palloc0(sizeof(FmgrInfo)); + fmgr_info(typinput, finfo); + jsestate->input.finfo = finfo; + } +} + +/* + * Evaluate a JSON error/empty behavior result. + */ +static Datum +GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null) +{ + *is_null = false; + + switch (behavior->btype) + { + case JSON_BEHAVIOR_EMPTY_ARRAY: + return JsonbPGetDatum(JsonbMakeEmptyArray()); + + case JSON_BEHAVIOR_EMPTY_OBJECT: + return JsonbPGetDatum(JsonbMakeEmptyObject()); + + case JSON_BEHAVIOR_TRUE: + return BoolGetDatum(true); + + case JSON_BEHAVIOR_FALSE: + return BoolGetDatum(false); + + case JSON_BEHAVIOR_NULL: + case JSON_BEHAVIOR_UNKNOWN: + case JSON_BEHAVIOR_EMPTY: + *is_null = true; + return (Datum) 0; + + case JSON_BEHAVIOR_DEFAULT: + /* Always handled in the caller. */ + Assert(false); + return (Datum) 0; + + default: + elog(ERROR, "unrecognized SQL/JSON behavior %d", behavior->btype); + return (Datum) 0; + } +} + +/* Initialize one JsonCoercion for execution. */ +static JsonCoercionState * +ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull) +{ + JsonCoercionState *jcstate = palloc0(sizeof(JsonCoercionState)); + + jcstate->coercion = coercion; + if (coercion && coercion->expr) + { + Datum *save_innermost_caseval; + bool *save_innermost_casenull; + + jcstate->jump_eval_expr = state->steps_len; + + /* Push step(s) to compute cstate->coercion. */ + save_innermost_caseval = state->innermost_caseval; + save_innermost_casenull = state->innermost_casenull; + + state->innermost_caseval = resv; + state->innermost_casenull = resnull; + + ExecInitExprRec((Expr *) coercion->expr, state, resv, resnull); + + state->innermost_caseval = save_innermost_caseval; + state->innermost_casenull = save_innermost_casenull; + } + else + jcstate->jump_eval_expr = -1; + + return jcstate; +} + +/* + * Push steps to evaluate coercions from a given JsonItemCoercions, which + * contains all possible coercions that may need to be applied to JSON + * items coming from evaluating the parent JsonExpr. + */ +static JsonItemCoercionsState * +ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull) +{ + JsonCoercion **coercion; + JsonItemCoercionsState *item_jcstates = palloc0(sizeof(JsonItemCoercionsState)); + JsonCoercionState **item_jcstate; + ExprEvalStep *as; + List *adjust_jumps = NIL; + ListCell *lc; + + /* Push the steps of individual coercions. */ + for (coercion = &coercions->null, + item_jcstate = &item_jcstates->null; + coercion <= &coercions->composite; + coercion++, item_jcstate++) + { + *item_jcstate = ExecInitJsonCoercion(scratch, state, *coercion, + resv, resnull); + + /* Emit JUMP step to skip past other coercions' steps. */ + scratch->opcode = EEOP_JUMP; + + /* + * Remember JUMP step address to set the actual jump target address + * below. + */ + adjust_jumps = lappend_int(adjust_jumps, state->steps_len); + ExprEvalPushStep(state, scratch); + } + + foreach(lc, adjust_jumps) + { + int jump_step_id = lfirst_int(lc); + + as = &state->steps[jump_step_id]; + as->d.jump.jumpdone = state->steps_len; + } + + return item_jcstates; +} diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 76e59691e5..4c97e714ea 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -74,6 +74,7 @@ #include "utils/json.h" #include "utils/jsonb.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/timestamp.h" @@ -152,6 +153,9 @@ static TupleDesc get_cached_rowtype(Oid type_id, int32 typmod, bool *changed); static void ExecEvalRowNullInt(ExprState *state, ExprEvalStep *op, ExprContext *econtext, bool checkisnull); +static Datum ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate); /* fast-path evaluation functions */ static Datum ExecJustInnerVar(ExprState *state, ExprContext *econtext, bool *isnull); @@ -480,6 +484,11 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_XMLEXPR, &&CASE_EEOP_JSON_CONSTRUCTOR, &&CASE_EEOP_IS_JSON, + &&CASE_EEOP_JSONEXPR_SKIP, + &&CASE_EEOP_JSONEXPR_PATH, + &&CASE_EEOP_JSONEXPR_BEHAVIOR, + &&CASE_EEOP_JSONEXPR_COERCION, + &&CASE_EEOP_JSONEXPR_COERCION_FINISH, &&CASE_EEOP_AGGREF, &&CASE_EEOP_GROUPING_FUNC, &&CASE_EEOP_WINDOW_FUNC, @@ -1186,8 +1195,14 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) /* second and third arguments are already set up */ fcinfo_in->isnull = false; + + /* Pass down soft-error capture node if any. */ + fcinfo_in->context = state->escontext; + d = FunctionCallInvoke(fcinfo_in); *op->resvalue = d; + if (SOFT_ERROR_OCCURRED(state->escontext)) + *op->resnull = true; /* Should get null result if and only if str is NULL */ if (str == NULL) @@ -1195,7 +1210,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) Assert(*op->resnull); Assert(fcinfo_in->isnull); } - else + else if (!SOFT_ERROR_OCCURRED(state->escontext)) { Assert(!*op->resnull); Assert(!fcinfo_in->isnull); @@ -1543,6 +1558,38 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_JSONEXPR_PATH) + { + /* too complex for an inline implementation */ + ExecEvalJsonExpr(state, op, econtext); + EEO_NEXT(); + } + + EEO_CASE(EEOP_JSONEXPR_SKIP) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprSkip(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_BEHAVIOR) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprBehavior(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercion(state, op, econtext, + *op->resvalue, *op->resnull)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION_FINISH) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercionFinish(state, op)); + } + EEO_CASE(EEOP_AGGREF) { /* @@ -3745,7 +3792,7 @@ ExecEvalConstraintCheck(ExprState *state, ExprEvalStep *op) { if (!*op->d.domaincheck.checknull && !DatumGetBool(*op->d.domaincheck.checkvalue)) - ereport(ERROR, + errsave(state->escontext, (errcode(ERRCODE_CHECK_VIOLATION), errmsg("value for domain %s violates check constraint \"%s\"", format_type_be(op->d.domaincheck.resulttype), @@ -4138,6 +4185,457 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op) *op->resvalue = BoolGetDatum(res); } +/* + * Evaluate given JsonExpr by performing the specified JSON operation. + * + * This also populates the JsonExprPostEvalState with the information needed + * by the subsequent steps that handle the specified JsonBehavior. + */ +void +ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) +{ + JsonExprState *jsestate = op->d.jsonexpr.jsestate; + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonExpr *jexpr = jsestate->jsexpr; + Datum item; + Datum res = (Datum) 0; + bool resnull = true; + JsonPath *path; + bool *error; + bool *empty; + + item = pre_eval->formatted_expr.value; + path = DatumGetJsonPathP(pre_eval->pathspec.value); + + /* Reset JsonExprPostEvalState for this evaluation. */ + memset(post_eval, 0, sizeof(*post_eval)); + + /* Respect ON ERROR behavior during path evaluation. */ + jsestate->throw_error = (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR); + + /* + * Be sure to save the error (if needed) and empty statuses for the + * EEOP_JSONEXPR_BEHAVIOR step to peruse. + */ + error = !jsestate->throw_error ? &post_eval->error : NULL; + empty = &post_eval->empty; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + res = JsonPathQuery(item, path, jexpr->wrapper, empty, error, + pre_eval->args); + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + resnull = !DatumGetPointer(res); + break; + + case JSON_VALUE_OP: + { + JsonbValue *jbv = JsonPathValue(item, path, empty, error, + pre_eval->args); + + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + if (!jbv) /* NULL or empty */ + { + resnull = true; + break; + } + + Assert(!*empty); + + resnull = false; + + /* coerce scalar item to the output type */ + if (jexpr->returning->typid == JSONOID || + jexpr->returning->typid == JSONBOID) + { + /* Use result coercion from json[b] to the output type */ + res = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + break; + } + + /* + * Error out if no cast exists to coerce SQL/JSON item to the + * the output type. + */ + Assert(post_eval->item_jcstate == NULL); + res = ExecPrepareJsonItemCoercion(jbv, + jsestate->item_jcstates, + &post_eval->item_jcstate); + if (post_eval->item_jcstate && + post_eval->item_jcstate->coercion && + (post_eval->item_jcstate->coercion->via_io || + post_eval->item_jcstate->coercion->via_populate)) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * Coercion via I/O means here that the cast to the target + * type simply does not exist. + */ + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE), + errmsg("SQL/JSON item cannot be cast to target type"))); + } + break; + } + + case JSON_EXISTS_OP: + { + bool exists = JsonPathExists(item, path, + pre_eval->args, + error); + + resnull = error && *error; + res = BoolGetDatum(exists); + break; + } + + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * If the ON EMPTY behavior is to cause an error, do so here. Other + * behaviors will be handled by the caller. + */ + if (*empty) + { + Assert(jexpr->on_empty); /* it is not JSON_EXISTS */ + + if (jexpr->on_empty->btype == JSON_BEHAVIOR_ERROR) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + ereport(ERROR, + (errcode(ERRCODE_NO_SQL_JSON_ITEM), + errmsg("no SQL/JSON item"))); + } + } + + *op->resvalue = res; + *op->resnull = resnull; +} + +/* + * Skip calling ExecEvalJson() on the given JsonExpr? + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_skip.jsestate; + + /* + * Skip if either of the input expressions has turned out to be NULL, + * though do execute domain checks for NULLs, which are handled by the + * coercion step. + */ + if (jsestate->pre_eval.formatted_expr.isnull || + jsestate->pre_eval.pathspec.isnull) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_skip.jump_coercion; + } + + /* + * Go evaluate the PASSING args if any and subsequently JSON path itself. + */ + return op->d.jsonexpr_skip.jump_passing_args; +} + +/* + * Returns the step address to perform the JsonBehavior applicable to + * the JSON item that resulted from evaluating the given JsonExpr. + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_behavior.jsestate; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonBehavior *behavior = NULL; + int jump_to = -1; + + if (post_eval->error || post_eval->coercion_error) + { + behavior = jsestate->jsexpr->on_error; + jump_to = op->d.jsonexpr_behavior.jump_onerror_expr; + } + else if (post_eval->empty) + { + behavior = jsestate->jsexpr->on_empty; + jump_to = op->d.jsonexpr_behavior.jump_onempty_expr; + } + else if (!post_eval->coercion_done) + { + /* + * If no error or the JSON item is not empty, directly go to the + * coercion step to coerce the item as is. + */ + return op->d.jsonexpr_behavior.jump_coercion; + } + + Assert(behavior); + + /* + * Set up for coercion step that will run to coerce a non-default behavior + * value. It should use result_coercion, if any. Errors that may occur + * should be thrown for JSON ops other than JSON_VALUE_OP. + */ + if (behavior->btype != JSON_BEHAVIOR_DEFAULT) + { + post_eval->item_jcstate = NULL; + jsestate->throw_error = (jsestate->jsexpr->op != JSON_VALUE_OP); + } + + Assert(jump_to >= 0); + return jump_to; +} + +/* + * Evaluate or return the step address to evaluate a coercion of a JSON item + * to the target type. The former if the coercion must be done right away by + * calling its input function or by calling json_populate_type(). + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull) +{ + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonExpr *jexpr = jsestate->jsexpr; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonCoercionState *item_jcstate = post_eval->item_jcstate; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + + if (item_jcstate == NULL && + jsestate->jsexpr->op != JSON_EXISTS_OP) + { + ErrorSaveContext escontext = {T_ErrorSaveContext}; + Node *escontext_p; + JsonCoercion *coercion; + Jsonb *jb; + + escontext_p = !jsestate->throw_error ? (Node *) &escontext : NULL; + coercion = result_jcstate ? result_jcstate->coercion : NULL; + jb = resnull ? NULL : DatumGetJsonbP(res); + + if ((coercion && coercion->via_io) || + (jexpr->omit_quotes && !resnull && + JB_ROOT_IS_SCALAR(jb))) + { + /* strip quotes and call typinput function */ + char *str = resnull ? NULL : JsonbUnquote(jb); + bool type_is_domain; + + type_is_domain = (getBaseType(jexpr->returning->typid) != + jexpr->returning->typid); + + /* + * Catch errors only if the type is not a domain, because errors + * caused by a domain's constraint failure must be thrown right + * away. + */ + if (!InputFunctionCallSafe(jsestate->input.finfo, str, + jsestate->input.typioparam, + jexpr->returning->typmod, + !type_is_domain ? escontext_p : NULL, + op->resvalue)) + { + post_eval->error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + else if (coercion && coercion->via_populate) + { + *op->resvalue = json_populate_type(res, JSONBOID, + jexpr->returning->typid, + jexpr->returning->typmod, + &post_eval->cache, + econtext->ecxt_per_query_memory, + op->resnull, + escontext_p); + if (SOFT_ERROR_OCCURRED(escontext_p)) + { + post_eval->error = true; + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + } + + if (!jsestate->throw_error) + { + post_eval->escontext.type = T_ErrorSaveContext; + state->escontext = (Node *) &post_eval->escontext; + } + + if (item_jcstate && item_jcstate->jump_eval_expr >= 0) + return item_jcstate->jump_eval_expr; + else if (result_jcstate && result_jcstate->jump_eval_expr >= 0) + return result_jcstate->jump_eval_expr; + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; +} + +/* + * Checks if the coercion evaluation led to an error. If an error did occur, + * this returns the address of the step that handles the error, otherwise + * the step after the coercion step, which finishes the JsonExpr processing. + */ +int +ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op) +{ + JsonExprPostEvalState *post_eval = + &op->d.jsonexpr_coercion_finish.jsestate->post_eval; + + if (SOFT_ERROR_OCCURRED(state->escontext)) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + post_eval->coercion_error = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_error; + } + + /* Reset. */ + state->escontext = NULL; + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_done; +} + +/* + * Prepare SQL/JSON item coercion to the output type. Returned a datum of the + * corresponding SQL type and a pointer to the coercion state. + */ +static Datum +ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate) +{ + JsonCoercionState *item_jcstate; + Datum res; + JsonbValue buf; + + if (item->type == jbvBinary && + JsonContainerIsScalar(item->val.binary.data)) + { + bool res PG_USED_FOR_ASSERTS_ONLY; + + res = JsonbExtractScalar(item->val.binary.data, &buf); + item = &buf; + Assert(res); + } + + /* get coercion state reference and datum of the corresponding SQL type */ + switch (item->type) + { + case jbvNull: + item_jcstate = item_jcstates->null; + res = (Datum) 0; + break; + + case jbvString: + item_jcstate = item_jcstates->string; + res = PointerGetDatum(cstring_to_text_with_len(item->val.string.val, + item->val.string.len)); + break; + + case jbvNumeric: + item_jcstate = item_jcstates->numeric; + res = NumericGetDatum(item->val.numeric); + break; + + case jbvBool: + item_jcstate = item_jcstates->boolean; + res = BoolGetDatum(item->val.boolean); + break; + + case jbvDatetime: + res = item->val.datetime.value; + switch (item->val.datetime.typid) + { + case DATEOID: + item_jcstate = item_jcstates->date; + break; + case TIMEOID: + item_jcstate = item_jcstates->time; + break; + case TIMETZOID: + item_jcstate = item_jcstates->timetz; + break; + case TIMESTAMPOID: + item_jcstate = item_jcstates->timestamp; + break; + case TIMESTAMPTZOID: + item_jcstate = item_jcstates->timestamptz; + break; + default: + elog(ERROR, "unexpected jsonb datetime type oid %u", + item->val.datetime.typid); + return (Datum) 0; + } + break; + + case jbvArray: + case jbvObject: + case jbvBinary: + item_jcstate = item_jcstates->composite; + res = JsonbPGetDatum(JsonbValueToJsonb(item)); + break; + + default: + elog(ERROR, "unexpected jsonb value type %d", item->type); + return (Datum) 0; + } + + *p_item_jcstate = item_jcstate; + + return res; +} + /* * ExecEvalGroupingFunc diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 00d7b8110b..da3870cba6 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -1860,6 +1860,256 @@ llvm_compile_expr(ExprState *state) LLVMBuildBr(b, opblocks[opno + 1]); break; + case EEOP_JSONEXPR_PATH: + build_EvalXFunc(b, mod, "ExecEvalJsonExpr", + v_state, op, v_econtext); + LLVMBuildBr(b, opblocks[opno + 1]); + break; + + case EEOP_JSONEXPR_SKIP: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprSkip() to decide if JSON path + * evaluation can be skipped. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprSkip"), + params, lengthof(params), ""); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_skip.jump_coercion, which signifies + * skipping of JSON path evaluation, else to the next step + * which must point to the steps to evaluate PASSING args, + * if any, or to the JSON path evaluation. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_skip.jump_coercion), + ""), + opblocks[op->d.jsonexpr_skip.jump_coercion], + opblocks[opno + 1]); + break; + } + + case EEOP_JSONEXPR_BEHAVIOR: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + LLVMBasicBlockRef b_jump_onerror_default; + + /* + * Call ExecEvalJsonExprBehavior() to decide if ON EMPTY + * or ON ERROR behavior must be invoked depending on what + * JSON path evaluation returned. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprBehavior"), + params, lengthof(params), ""); + + b_jump_onerror_default = + l_bb_before_v(opblocks[opno + 1], + "op.%d.jsonexpr_behavior_jump_onerror_default", opno); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_behavior.jump_coercion, else to the + * next block, one that checks whether to evaluate the ON + * ERROR default expression. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_coercion), + ""), + opblocks[op->d.jsonexpr_behavior.jump_coercion], + b_jump_onerror_default); + + /* + * Block that checks whether to evaluate the ON ERROR + * default expression. + * + * Jump to evaluate the ON ERROR default expression if the + * returned address is the same as + * jsonexpr_behavior.jump_onerror_default, else jump to + * evaluate the ON EMPTY default expression. + */ + LLVMPositionBuilderAtEnd(b, b_jump_onerror_default); + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_onerror_expr), + ""), + opblocks[op->d.jsonexpr_behavior.jump_onerror_expr], + opblocks[op->d.jsonexpr_behavior.jump_onempty_expr]); + break; + } + case EEOP_JSONEXPR_COERCION: + { + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonItemCoercionsState *item_jcstates = jsestate->item_jcstates; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + LLVMValueRef v_ret; + LLVMValueRef params[5]; + + /* + * Call ExecEvalJsonExprCoercion() to evaluate appropriate + * coercion. This will return the step address to jump + * to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + params[2] = v_econtext; + params[3] = v_resvaluep; + params[4] = v_resnullp; + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercion"), + params, lengthof(params), ""); + + /* + * Jump to handle a coercion error if the returned address + * is the same as jsonexpr_coercion.jump_coercion_error, + * else to the step after coercion (coercion done!). + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + + if (item_jcstates) + { + JsonCoercionState **item_jcstate; + int n_coercions = (int) + (item_jcstates->composite - item_jcstates->null) + 1; + int i; + LLVMBasicBlockRef *b_coercions; + + + /* + * Will create a block for each coercion below to + * check whether to evaluate the coercion's expression + * if there's one or to skip to the end if not. + */ + b_coercions = palloc((n_coercions + 1) * sizeof(LLVMBasicBlockRef)); + for (i = 0; i < n_coercions + 1; i++) + b_coercions[i] = + l_bb_before_v(opblocks[opno + 1], + "op.%d.json_item_coercion.%d", + opno, i); + + /* Jump to check first coercion */ + LLVMBuildBr(b, b_coercions[0]); + + /* + * Add conditional branches for individual coercion's + * expressions + */ + for (item_jcstate = &item_jcstates->null, i = 0; + item_jcstate <= &item_jcstates->composite; + item_jcstate++, i++) + { + /* Block for this coercion */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + + /* + * Jump to evaluate the coercion's expression if + * the address returned is the same as this + * coercion's jump_eval_expr (that is, if it is + * valid), else check the next coercion's. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const((*item_jcstate)->jump_eval_expr), + ""), + (*item_jcstate)->jump_eval_expr >= 0 ? + opblocks[(*item_jcstate)->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + b_coercions[i + 1]); + } + + /* + * A placeholder block that the last coercion's block + * might jump to, which unconditionally jumps to end + * of coercions. + */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + LLVMBuildBr(b, opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + + /* + * Jump to evaluate the result_coercion's expression if + * none of the above coercions matched, that is, if + * there's one. + */ + if (result_jcstate) + { + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(result_jcstate->jump_eval_expr), + ""), + result_jcstate->jump_eval_expr >= 0 ? + opblocks[result_jcstate->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + break; + } + + case EEOP_JSONEXPR_COERCION_FINISH: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprCoercionFinish() to check whether + * an coercion error occurred, in which case we must jump + * to whatever step handles the error. This returns the + * step address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercionFinish"), + params, lengthof(params), ""); + + /* + * Jump to the step that handles coercion error if the + * returned address is the same as + * jsonexpr_coercion_finish.jump_coercion_error, else to + * jsonexpr_coercion_finish.jump_coercion_done. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion_finish.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_done]); + break; + } + case EEOP_AGGREF: { LLVMValueRef v_aggno; diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c index 41ac4c6f45..cf3ced3427 100644 --- a/src/backend/jit/llvm/llvmjit_types.c +++ b/src/backend/jit/llvm/llvmjit_types.c @@ -135,6 +135,11 @@ void *referenced_functions[] = ExecEvalXmlExpr, ExecEvalJsonConstructor, ExecEvalJsonIsPredicate, + ExecEvalJsonExprSkip, + ExecEvalJsonExprBehavior, + ExecEvalJsonExprCoercion, + ExecEvalJsonExprCoercionFinish, + ExecEvalJsonExpr, MakeExpandedObjectReadOnlyInternal, slot_getmissingattrs, slot_getsomeattrs_int, diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index c6c310d253..7bcc12b04f 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -863,6 +863,21 @@ makeJsonValueExpr(Expr *expr, JsonFormat *format) return jve; } +/* + * makeJsonBehavior - + * creates a JsonBehavior node + */ +JsonBehavior * +makeJsonBehavior(JsonBehaviorType type, Node *default_expr) +{ + JsonBehavior *behavior = makeNode(JsonBehavior); + + behavior->btype = type; + behavior->default_expr = default_expr; + + return behavior; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index dda964bd19..d31371bb4d 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -236,6 +236,12 @@ exprType(const Node *expr) case T_JsonIsPredicate: type = BOOLOID; break; + case T_JsonExpr: + type = ((const JsonExpr *) expr)->returning->typid; + break; + case T_JsonCoercion: + type = exprType(((const JsonCoercion *) expr)->expr); + break; case T_NullTest: type = BOOLOID; break; @@ -495,6 +501,10 @@ exprTypmod(const Node *expr) return exprTypmod((Node *) ((const JsonValueExpr *) expr)->formatted_expr); case T_JsonConstructorExpr: return ((const JsonConstructorExpr *) expr)->returning->typmod; + case T_JsonExpr: + return ((JsonExpr *) expr)->returning->typmod; + case T_JsonCoercion: + return exprTypmod(((const JsonCoercion *) expr)->expr); case T_CoerceToDomain: return ((const CoerceToDomain *) expr)->resulttypmod; case T_CoerceToDomainValue: @@ -971,6 +981,22 @@ exprCollation(const Node *expr) /* IS JSON's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + coll = InvalidOid; + else if (coercion->expr) + coll = exprCollation(coercion->expr); + else if (coercion->via_io || coercion->via_populate) + coll = coercion->collation; + else + coll = InvalidOid; + } + break; + case T_NullTest: /* NullTest's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ @@ -1207,6 +1233,21 @@ exprSetCollation(Node *expr, Oid collation) case T_JsonIsPredicate: Assert(!OidIsValid(collation)); /* result is always boolean */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + Assert(!OidIsValid(collation)); + else if (coercion->expr) + exprSetCollation(coercion->expr, collation); + else if (coercion->via_io || coercion->via_populate) + coercion->collation = collation; + else + Assert(!OidIsValid(collation)); + } + break; case T_NullTest: /* NullTest's result is boolean ... */ Assert(!OidIsValid(collation)); /* ... so never set a collation */ @@ -1510,6 +1551,15 @@ exprLocation(const Node *expr) case T_JsonIsPredicate: loc = ((const JsonIsPredicate *) expr)->location; break; + case T_JsonExpr: + { + const JsonExpr *jsexpr = (const JsonExpr *) expr; + + /* consider both function name and leftmost arg */ + loc = leftmostLoc(jsexpr->location, + exprLocation(jsexpr->formatted_expr)); + } + break; case T_NullTest: { const NullTest *nexpr = (const NullTest *) expr; @@ -2262,6 +2312,54 @@ expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + if (WALK(jexpr->formatted_expr)) + return true; + if (WALK(jexpr->result_coercion)) + return true; + if (WALK(jexpr->passing_values)) + return true; + /* we assume walker doesn't care about passing_names */ + if (jexpr->on_empty && + WALK(jexpr->on_empty->default_expr)) + return true; + if (WALK(jexpr->on_error->default_expr)) + return true; + if (WALK(jexpr->coercions)) + return true; + } + break; + case T_JsonCoercion: + return WALK(((JsonCoercion *) node)->expr); + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + + if (WALK(coercions->null)) + return true; + if (WALK(coercions->string)) + return true; + if (WALK(coercions->numeric)) + return true; + if (WALK(coercions->boolean)) + return true; + if (WALK(coercions->date)) + return true; + if (WALK(coercions->time)) + return true; + if (WALK(coercions->timetz)) + return true; + if (WALK(coercions->timestamp)) + return true; + if (WALK(coercions->timestamptz)) + return true; + if (WALK(coercions->composite)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: @@ -3261,6 +3359,54 @@ expression_tree_mutator_impl(Node *node, return (Node *) newnode; } + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + JsonExpr *newnode; + + FLATCOPY(newnode, jexpr, JsonExpr); + MUTATE(newnode->path_spec, jexpr->path_spec, Node *); + MUTATE(newnode->formatted_expr, jexpr->formatted_expr, Node *); + MUTATE(newnode->result_coercion, jexpr->result_coercion, JsonCoercion *); + MUTATE(newnode->passing_values, jexpr->passing_values, List *); + /* assume mutator does not care about passing_names */ + if (newnode->on_empty) + MUTATE(newnode->on_empty->default_expr, + jexpr->on_empty->default_expr, Node *); + MUTATE(newnode->on_error->default_expr, + jexpr->on_error->default_expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonCoercion: + { + JsonCoercion *coercion = (JsonCoercion *) node; + JsonCoercion *newnode; + + FLATCOPY(newnode, coercion, JsonCoercion); + MUTATE(newnode->expr, coercion->expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + JsonItemCoercions *newnode; + + FLATCOPY(newnode, coercions, JsonItemCoercions); + MUTATE(newnode->null, coercions->null, JsonCoercion *); + MUTATE(newnode->string, coercions->string, JsonCoercion *); + MUTATE(newnode->numeric, coercions->numeric, JsonCoercion *); + MUTATE(newnode->boolean, coercions->boolean, JsonCoercion *); + MUTATE(newnode->date, coercions->date, JsonCoercion *); + MUTATE(newnode->time, coercions->time, JsonCoercion *); + MUTATE(newnode->timetz, coercions->timetz, JsonCoercion *); + MUTATE(newnode->timestamp, coercions->timestamp, JsonCoercion *); + MUTATE(newnode->timestamptz, coercions->timestamptz, JsonCoercion *); + MUTATE(newnode->composite, coercions->composite, JsonCoercion *); + return (Node *) newnode; + } + break; case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -3947,6 +4093,43 @@ raw_expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonArgument: + return WALK(((JsonArgument *) node)->val); + case T_JsonCommon: + { + JsonCommon *jc = (JsonCommon *) node; + + if (WALK(jc->expr)) + return true; + if (WALK(jc->pathspec)) + return true; + if (WALK(jc->passing)) + return true; + } + break; + case T_JsonBehavior: + { + JsonBehavior *jb = (JsonBehavior *) node; + + if (jb->btype == JSON_BEHAVIOR_DEFAULT && + WALK(jb->default_expr)) + return true; + } + break; + case T_JsonFuncExpr: + { + JsonFuncExpr *jfe = (JsonFuncExpr *) node; + + if (WALK(jfe->common)) + return true; + if (jfe->output && WALK(jfe->output)) + return true; + if (WALK(jfe->on_empty)) + return true; + if (WALK(jfe->on_error)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index ef475d95a1..f58c275b4b 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -4609,7 +4609,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context) IsA(node, SQLValueFunction) || IsA(node, XmlExpr) || IsA(node, CoerceToDomain) || - IsA(node, NextValueExpr)) + IsA(node, NextValueExpr) || + IsA(node, JsonExpr)) { /* Treat all these as having cost 1 */ context->total.per_tuple += cpu_operator_cost; diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index da258968b8..e40cfab4b7 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -53,6 +53,7 @@ #include "utils/fmgroids.h" #include "utils/json.h" #include "utils/jsonb.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/syscache.h" @@ -412,6 +413,24 @@ contain_mutable_functions_walker(Node *node, void *context) /* Check all subnodes */ } + if (IsA(node, JsonExpr)) + { + JsonExpr *jexpr = castNode(JsonExpr, node); + Const *cnst; + + if (!IsA(jexpr->path_spec, Const)) + return true; + + cnst = castNode(Const, jexpr->path_spec); + + Assert(cnst->consttype == JSONPATHOID); + if (cnst->constisnull) + return false; + + return jspIsMutable(DatumGetJsonPathP(cnst->constvalue), + jexpr->passing_names, jexpr->passing_values); + } + if (IsA(node, SQLValueFunction)) { /* all variants of SQLValueFunction are stable */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 341b002dc7..f7ad8f18de 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -278,6 +278,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MergeWhenClause *mergewhen; struct KeyActions *keyactions; struct KeyAction *keyaction; + JsonBehavior *jsbehavior; + JsonQuotes js_quotes; } %type <node> stmt toplevel_stmt schema_stmt routine_body_stmt @@ -650,14 +652,22 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_returning_clause_opt json_name_and_value json_aggregate_func + json_api_common_syntax + json_argument %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt + json_arguments %type <ival> json_encoding_clause_opt json_predicate_type_constraint + json_wrapper_behavior %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt +%type <jsbehavior> json_value_behavior + json_query_behavior + json_exists_behavior +%type <js_quotes> json_quotes_clause_opt /* * Non-keyword token types. These are hard-wired into the "flex" lexer. @@ -694,7 +704,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT - COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT + COMMITTED COMPRESSION CONCURRENTLY CONDITIONAL CONFIGURATION CONFLICT CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY COST CREATE CROSS CSV CUBE CURRENT_P CURRENT_CATALOG CURRENT_DATE CURRENT_ROLE CURRENT_SCHEMA @@ -705,8 +715,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P DOUBLE_P DROP - EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT - EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION + EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE + EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION EXTENSION EXTERNAL EXTRACT FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR @@ -721,10 +731,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION - JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG - JSON_SCALAR JSON_SERIALIZE + JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE - KEY KEYS + KEY KEYS KEEP LABEL LANGUAGE LARGE_P LAST_P LATERAL_P LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL @@ -738,7 +748,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC - OBJECT_P OF OFF OFFSET OIDS OLD ON ONLY OPERATOR OPTION OPTIONS OR + OBJECT_P OF OFF OFFSET OIDS OLD OMIT ON ONLY OPERATOR OPTION OPTIONS OR ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER @@ -747,7 +757,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION - QUOTE + QUOTE QUOTES RANGE READ REAL REASSIGN RECHECK RECURSIVE REF_P REFERENCES REFERENCING REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA @@ -758,7 +768,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); SEQUENCE SEQUENCES SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P - START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P + START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRING_P STRIP_P SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN @@ -766,7 +776,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); TREAT TRIGGER TRIM TRUE_P TRUNCATE TRUSTED TYPE_P TYPES_P - UESCAPE UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN + UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING @@ -15663,6 +15673,192 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_error = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->on_error = $10; + n->location = @1; + $$ = (Node *) n; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->location = @1; + $$ = (Node *) p; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + json_exists_behavior ON ERROR_P + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->on_error = $5; + p->location = @1; + $$ = (Node *) p; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->on_error = $8; + n->location = @1; + $$ = (Node *) n; + } ; @@ -16389,6 +16585,72 @@ opt_asymmetric: ASYMMETRIC ; /* SQL/JSON support */ +json_api_common_syntax: + json_value_expr ',' a_expr /* i.e. a json_path */ + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + + | json_value_expr ',' a_expr /* i.e. a json_path */ + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = $5; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = $7; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_arguments: + json_argument { $$ = list_make1($1); } + | json_arguments ',' json_argument { $$ = lappend($1, $3); } + ; + +json_argument: + json_value_expr AS ColLabel + { + JsonArgument *n = makeNode(JsonArgument); + + n->val = (JsonValueExpr *) $1; + n->name = $3; + $$ = (Node *) n; + } + ; + json_value_expr: a_expr json_format_clause_opt { @@ -16412,6 +16674,50 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; +json_query_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + | EMPTY_P ARRAY { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + | EMPTY_P OBJECT_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + /* non-standard, for Oracle compatibility only */ + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + ; + +json_exists_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | TRUE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_TRUE, NULL); } + | FALSE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_FALSE, NULL); } + | UNKNOWN { $$ = makeJsonBehavior(JSON_BEHAVIOR_UNKNOWN, NULL); } + ; + +json_value_behavior: + NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + ; + +/* ARRAY is a noise word */ +json_wrapper_behavior: + WITHOUT WRAPPER { $$ = JSW_NONE; } + | WITHOUT ARRAY WRAPPER { $$ = JSW_NONE; } + | WITH WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL ARRAY WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL WRAPPER { $$ = JSW_UNCONDITIONAL; } + | /* empty */ { $$ = JSW_NONE; } + ; + +json_quotes_clause_opt: + KEEP QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_KEEP; } + | KEEP QUOTES { $$ = JS_QUOTES_KEEP; } + | OMIT QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_OMIT; } + | OMIT QUOTES { $$ = JS_QUOTES_OMIT; } + | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } + ; + json_returning_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17014,6 +17320,7 @@ unreserved_keyword: | COMMIT | COMMITTED | COMPRESSION + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17050,10 +17357,12 @@ unreserved_keyword: | DOUBLE_P | DROP | EACH + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17103,6 +17412,7 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION + | KEEP | KEY | KEYS | LABEL @@ -17149,6 +17459,7 @@ unreserved_keyword: | OFF | OIDS | OLD + | OMIT | OPERATOR | OPTION | OPTIONS @@ -17179,6 +17490,7 @@ unreserved_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REASSIGN @@ -17238,6 +17550,7 @@ unreserved_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUPPORT @@ -17260,6 +17573,7 @@ unreserved_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNKNOWN | UNLISTEN @@ -17320,10 +17634,13 @@ col_name_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE | LEAST | NATIONAL | NCHAR @@ -17556,6 +17873,7 @@ bare_label_keyword: | COMMITTED | COMPRESSION | CONCURRENTLY + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17608,11 +17926,13 @@ bare_label_keyword: | DROP | EACH | ELSE + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | END_P | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17682,10 +18002,14 @@ bare_label_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE + | KEEP | KEY | KEYS | LABEL @@ -17746,6 +18070,7 @@ bare_label_keyword: | OFF | OIDS | OLD + | OMIT | ONLY | OPERATOR | OPTION @@ -17783,6 +18108,7 @@ bare_label_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REAL @@ -17851,6 +18177,7 @@ bare_label_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUBSTRING @@ -17885,6 +18212,7 @@ bare_label_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNIQUE | UNKNOWN diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c index 9f6afc351c..692e5d1225 100644 --- a/src/backend/parser/parse_collate.c +++ b/src/backend/parser/parse_collate.c @@ -691,6 +691,13 @@ assign_collations_walker(Node *node, assign_collations_context *context) &loccontext); } break; + case T_JsonExpr: + + /* + * Context item and PASSING arguments are already + * marked with collations in parse_expr.c. + */ + break; default: /* diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index b30094af84..2ff2b4d01f 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -90,6 +90,7 @@ static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); static Node *transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr); +static Node *transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *p); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -353,6 +354,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); break; + case T_JsonFuncExpr: + result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3225,7 +3230,7 @@ makeJsonByteaToTextConversion(Node *expr, JsonFormat *format, int location) static Node * transformJsonValueExpr(ParseState *pstate, char *constructName, JsonValueExpr *ve, JsonFormatType default_format, - Oid targettype) + Oid targettype, bool isarg) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3262,6 +3267,35 @@ transformJsonValueExpr(ParseState *pstate, char *constructName, else format = ve->format->format_type; } + else if (isarg) + { + /* Pass SQL/JSON item types directly without conversion to json[b]. */ + switch (exprtype) + { + case TEXTOID: + case NUMERICOID: + case BOOLOID: + case INT2OID: + case INT4OID: + case INT8OID: + case FLOAT4OID: + case FLOAT8OID: + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + return expr; + + default: + if (typcategory == TYPCATEGORY_STRING) + return expr; + /* else convert argument to json[b] type */ + break; + } + + format = default_format; + } else if (exprtype == JSONOID || exprtype == JSONBOID) format = JS_FORMAT_DEFAULT; /* do not format json[b] types */ else @@ -3273,7 +3307,8 @@ transformJsonValueExpr(ParseState *pstate, char *constructName, Node *coerced; bool cast_is_needed = OidIsValid(targettype); - if (!cast_is_needed && + if (!isarg && + !cast_is_needed && exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), @@ -3614,7 +3649,7 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) Node *val = transformJsonValueExpr(pstate, "JSON_OBJECT()", kv->value, JS_FORMAT_DEFAULT, - InvalidOid); + InvalidOid, false); args = lappend(args, key); args = lappend(args, val); @@ -3799,7 +3834,7 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) key = transformExprRecurse(pstate, (Node *) agg->arg->key); val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", agg->arg->value, - JS_FORMAT_DEFAULT, InvalidOid); + JS_FORMAT_DEFAULT, InvalidOid, false); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3855,9 +3890,8 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) Oid aggfnoid; Oid aggtype; - arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", - agg->arg, - JS_FORMAT_DEFAULT, InvalidOid); + arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", agg->arg, + JS_FORMAT_DEFAULT, InvalidOid, false); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3904,9 +3938,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) { JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, "JSON_ARRAY()", - jsval, - JS_FORMAT_DEFAULT, - InvalidOid); + jsval, JS_FORMAT_DEFAULT, + InvalidOid, false); args = lappend(args, val); } @@ -4077,7 +4110,7 @@ transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) * function-like CASTs. */ arg = transformJsonValueExpr(pstate, "JSON()", jsexpr->expr, - JS_FORMAT_JSON, returning->typid); + JS_FORMAT_JSON, returning->typid, false); } return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, @@ -4124,9 +4157,8 @@ static Node * transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) { Node *arg = transformJsonValueExpr(pstate, "JSON_SERIALIZE()", - expr->expr, - JS_FORMAT_JSON, - InvalidOid); + expr->expr, JS_FORMAT_JSON, + InvalidOid, false); JsonReturning *returning; if (expr->output) @@ -4161,3 +4193,462 @@ transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), NULL, returning, false, false, expr->location); } + +/* + * Transform a JSON PASSING clause. + */ +static void +transformJsonPassingArgs(ParseState *pstate, char *constructName, + JsonFormatType format, List *args, + List **passing_values, List **passing_names) +{ + ListCell *lc; + + *passing_values = NIL; + *passing_names = NIL; + + foreach(lc, args) + { + JsonArgument *arg = castNode(JsonArgument, lfirst(lc)); + Node *expr = transformJsonValueExpr(pstate, constructName, + arg->val, format, + InvalidOid, true); + + assign_expr_collations(pstate, expr); + + *passing_values = lappend(*passing_values, expr); + *passing_names = lappend(*passing_names, makeString(arg->name)); + } +} + +/* + * Transform a JSON BEHAVIOR clause. + */ +static JsonBehavior * +transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior, + JsonBehaviorType default_behavior) +{ + JsonBehaviorType behavior_type = default_behavior; + Node *default_expr = NULL; + + if (behavior) + { + behavior_type = behavior->btype; + if (behavior_type == JSON_BEHAVIOR_DEFAULT) + default_expr = transformExprRecurse(pstate, behavior->default_expr); + } + return makeJsonBehavior(behavior_type, default_expr); +} + +/* + * Common code for JSON_VALUE, JSON_QUERY, JSON_EXISTS transformation + * into a JsonExpr node. + */ +static JsonExpr * +transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = makeNode(JsonExpr); + Node *pathspec; + JsonFormatType format; + char *constructName; + + if (func->common->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("JSON_TABLE path name is not allowed here"), + parser_errposition(pstate, func->location))); + + jsexpr->location = func->location; + jsexpr->op = func->op; + switch (jsexpr->op) + { + case JSON_VALUE_OP: + constructName = "JSON_VALUE()"; + break; + case JSON_QUERY_OP: + constructName = "JSON_QUERY()"; + break; + case JSON_EXISTS_OP: + constructName = "JSON_EXISTS()"; + break; + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); + break; + } + jsexpr->formatted_expr = transformJsonValueExpr(pstate, constructName, + func->common->expr, + JS_FORMAT_DEFAULT, + InvalidOid, false); + + assign_expr_collations(pstate, jsexpr->formatted_expr); + + /* format is determined by context item type */ + format = exprType(jsexpr->formatted_expr) == JSONBOID ? JS_FORMAT_JSONB : JS_FORMAT_JSON; + + jsexpr->result_coercion = NULL; + jsexpr->omit_quotes = false; + + jsexpr->format = func->common->expr->format; + + pathspec = transformExprRecurse(pstate, func->common->pathspec); + + jsexpr->path_spec = + coerce_to_target_type(pstate, pathspec, exprType(pathspec), + JSONPATHOID, -1, + COERCION_EXPLICIT, COERCE_IMPLICIT_CAST, + exprLocation(pathspec)); + if (!jsexpr->path_spec) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("JSON path expression must be type %s, not type %s", + "jsonpath", format_type_be(exprType(pathspec))), + parser_errposition(pstate, exprLocation(pathspec)))); + + /* transform and coerce to json[b] passing arguments */ + transformJsonPassingArgs(pstate, constructName, format, + func->common->passing, + &jsexpr->passing_values, + &jsexpr->passing_names); + + if (func->op != JSON_EXISTS_OP) + jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, + JSON_BEHAVIOR_NULL); + + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + func->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : + JSON_BEHAVIOR_NULL); + + return jsexpr; +} + +/* + * Assign default JSON returning type from the specified format or from + * the context item type. + */ +static void +assignDefaultJsonReturningType(Node *context_item, JsonFormat *context_format, + JsonReturning *ret) +{ + bool is_jsonb; + + ret->format = copyObject(context_format); + + if (ret->format->format_type == JS_FORMAT_DEFAULT) + is_jsonb = exprType(context_item) == JSONBOID; + else + is_jsonb = ret->format->format_type == JS_FORMAT_JSONB; + + ret->typid = is_jsonb ? JSONBOID : JSONOID; + ret->typmod = -1; +} + +/* + * Try to coerce expression to the output type or + * use json_populate_type() for composite, array and domain types or + * use coercion via I/O. + */ +static JsonCoercion * +coerceJsonExpr(ParseState *pstate, Node *expr, const JsonReturning *returning) +{ + char typtype; + JsonCoercion *coercion = makeNode(JsonCoercion); + + coercion->expr = coerceJsonFuncExpr(pstate, expr, returning, false); + + if (coercion->expr) + { + if (coercion->expr == expr) + coercion->expr = NULL; + + return coercion; + } + + typtype = get_typtype(returning->typid); + + if (returning->typid == RECORDOID || + typtype == TYPTYPE_COMPOSITE || + typtype == TYPTYPE_DOMAIN || + type_is_array(returning->typid)) + coercion->via_populate = true; + else + coercion->via_io = true; + + return coercion; +} + +/* + * Transform a JSON output clause of JSON_VALUE and JSON_QUERY. + */ +static void +transformJsonFuncExprOutput(ParseState *pstate, JsonFuncExpr *func, + JsonExpr *jsexpr) +{ + Node *expr = jsexpr->formatted_expr; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + /* JSON_VALUE returns text by default */ + if (func->op == JSON_VALUE_OP && !OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = TEXTOID; + jsexpr->returning->typmod = -1; + } + + if (OidIsValid(jsexpr->returning->typid)) + { + JsonReturning ret; + + if (func->op == JSON_VALUE_OP && + jsexpr->returning->typid != JSONOID && + jsexpr->returning->typid != JSONBOID) + { + /* Forced coercion via I/O for JSON_VALUE for non-JSON types */ + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = NULL; + jsexpr->result_coercion->via_io = true; + return; + } + + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, &ret); + + if (ret.typid != jsexpr->returning->typid || + ret.typmod != jsexpr->returning->typmod) + { + CaseTestExpr *cte = makeNode(CaseTestExpr); + + cte->typeId = exprType(expr); + cte->typeMod = exprTypmod(expr); + cte->collation = exprCollation(expr); + + Assert(cte->typeId == ret.typid); + Assert(cte->typeMod == ret.typmod); + + jsexpr->result_coercion = coerceJsonExpr(pstate, (Node *) cte, + jsexpr->returning); + } + } + else + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, + jsexpr->returning); +} + +/* + * Coerce an expression in JSON DEFAULT behavior to the target output type. + */ +static Node * +coerceDefaultJsonExpr(ParseState *pstate, JsonExpr *jsexpr, Node *defexpr) +{ + int location; + Oid exprtype; + + if (!defexpr) + return NULL; + + exprtype = exprType(defexpr); + location = exprLocation(defexpr); + + if (location < 0) + location = jsexpr->location; + + defexpr = coerce_to_target_type(pstate, + defexpr, + exprtype, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!defexpr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast DEFAULT expression type %s to %s", + format_type_be(exprtype), + format_type_be(jsexpr->returning->typid)), + parser_errposition(pstate, location))); + + return defexpr; +} + +/* + * Initialize SQL/JSON item coercion from the SQL type "typid" to the target + * "returning" type. + */ +static JsonCoercion * +initJsonItemCoercion(ParseState *pstate, Oid typid, + const JsonReturning *returning) +{ + Node *expr; + + if (typid == UNKNOWNOID) + { + expr = (Node *) makeNullConst(UNKNOWNOID, -1, InvalidOid); + } + else + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + + placeholder->typeId = typid; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + expr = (Node *) placeholder; + } + + return coerceJsonExpr(pstate, expr, returning); +} + +static void +initJsonItemCoercions(ParseState *pstate, JsonItemCoercions *coercions, + const JsonReturning *returning, Oid contextItemTypeId) +{ + struct + { + JsonCoercion **coercion; + Oid typid; + } *p, + coercionTypids[] = + { + {&coercions->null, UNKNOWNOID}, + {&coercions->string, TEXTOID}, + {&coercions->numeric, NUMERICOID}, + {&coercions->boolean, BOOLOID}, + {&coercions->date, DATEOID}, + {&coercions->time, TIMEOID}, + {&coercions->timetz, TIMETZOID}, + {&coercions->timestamp, TIMESTAMPOID}, + {&coercions->timestamptz, TIMESTAMPTZOID}, + {&coercions->composite, contextItemTypeId}, + {NULL, InvalidOid} + }; + + for (p = coercionTypids; p->coercion; p++) + *p->coercion = initJsonItemCoercion(pstate, p->typid, returning); +} + +/* + * Transform JSON_VALUE, JSON_QUERY, JSON_EXISTS functions into a JsonExpr node. + */ +static Node * +transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = transformJsonExprCommon(pstate, func); + const char *func_name = NULL; + Node *contextItemExpr = jsexpr->formatted_expr; + + /* + * Disallow FORMAT specification in the RETURNING clause of JSON_EXISTS() + * and JSON_VALUE(). + */ + if (func->output && + (func->op == JSON_VALUE_OP || func->op == JSON_EXISTS_OP)) + { + JsonFormat *format = func->output->returning->format; + + if(format->format_type != JS_FORMAT_DEFAULT || + format->encoding != JS_ENC_DEFAULT) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot specify FORMAT in RETURNING clause of %s", + func->op == JSON_VALUE_OP ? "JSON_VALUE()" : + "JSON_EXISTS()"), + parser_errposition(pstate, format->location))); + } + + switch (func->op) + { + case JSON_VALUE_OP: + func_name = "JSON_VALUE"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->coercions = makeNode(JsonItemCoercions); + initJsonItemCoercions(pstate, jsexpr->coercions, jsexpr->returning, + exprType(contextItemExpr)); + + break; + + case JSON_QUERY_OP: + func_name = "JSON_QUERY"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->wrapper = func->wrapper; + jsexpr->omit_quotes = func->omit_quotes; + + break; + + case JSON_EXISTS_OP: + func_name = "JSON_EXISTS"; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + if (!OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = BOOLOID; + jsexpr->returning->typmod = -1; + } + else if (jsexpr->returning->typid != BOOLOID) + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + int location = exprLocation((Node *) jsexpr); + + placeholder->typeId = BOOLOID; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = + coerce_to_target_type(pstate, (Node *) placeholder, BOOLOID, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!jsexpr->result_coercion->expr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(BOOLOID), + format_type_be(jsexpr->returning->typid)), + parser_coercion_errposition(pstate, location, (Node *) jsexpr))); + + if (jsexpr->result_coercion->expr == (Node *) placeholder) + jsexpr->result_coercion->expr = NULL; + } + break; + } + + if (exprType(contextItemExpr) != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("%s() is not yet implemented for the json type", func_name), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + return (Node *) jsexpr; +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 520d4f2a23..4f94fc69d6 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1979,6 +1979,21 @@ FigureColnameInternal(Node *node, char **name) /* make JSON_ARRAYAGG act like a regular function */ *name = "json_arrayagg"; return 2; + case T_JsonFuncExpr: + /* make SQL/JSON functions act like a regular function */ + switch (((JsonFuncExpr *) node)->op) + { + case JSON_QUERY_OP: + *name = "json_query"; + return 2; + case JSON_VALUE_OP: + *name = "json_value"; + return 2; + case JSON_EXISTS_OP: + *name = "json_exists"; + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index e27ea8ef97..188b5594e0 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -4426,6 +4426,50 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, } } +/* + * Parses the datetime format string in 'fmt_str' and returns true if it + * contains a timezone specifier, false if not. + */ +bool +datetime_format_has_tz(const char *fmt_str) +{ + bool incache; + int fmt_len = strlen(fmt_str); + int result; + FormatNode *format; + + if (fmt_len > DCH_CACHE_SIZE) + { + /* + * Allocate new memory if format picture is bigger than static cache + * and do not use cache (call parser always) + */ + incache = false; + + format = (FormatNode *) palloc((fmt_len + 1) * sizeof(FormatNode)); + + parse_format(format, fmt_str, DCH_keywords, + DCH_suff, DCH_index, DCH_FLAG, NULL); + } + else + { + /* + * Use cache buffers + */ + DCHCacheEntry *ent = DCH_cache_fetch(fmt_str, false); + + incache = true; + format = ent->format; + } + + result = DCH_datetime_type(format); + + if (!incache) + pfree(format); + + return result & DCH_ZONED; +} + /* * do_to_timestamp: shared code for to_timestamp and to_date * diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index 06ba409e64..d2b4da8ec8 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -2156,3 +2156,65 @@ jsonb_float8(PG_FUNCTION_ARGS) PG_RETURN_DATUM(retValue); } + +/* + * Construct an empty array jsonb. + */ +Jsonb * +JsonbMakeEmptyArray(void) +{ + JsonbValue jbv; + + jbv.type = jbvArray; + jbv.val.array.elems = NULL; + jbv.val.array.nElems = 0; + jbv.val.array.rawScalar = false; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Construct an empty object jsonb. + */ +Jsonb * +JsonbMakeEmptyObject(void) +{ + JsonbValue jbv; + + jbv.type = jbvObject; + jbv.val.object.pairs = NULL; + jbv.val.object.nPairs = 0; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Convert jsonb to a C-string stripping quotes from scalar strings. + */ +char * +JsonbUnquote(Jsonb *jb) +{ + if (JB_ROOT_IS_SCALAR(jb)) + { + JsonbValue v; + + (void) JsonbExtractScalar(&jb->root, &v); + + if (v.type == jbvString) + return pnstrdup(v.val.string.val, v.val.string.len); + else if (v.type == jbvBool) + return pstrdup(v.val.boolean ? "true" : "false"); + else if (v.type == jbvNumeric) + return DatumGetCString(DirectFunctionCall1(numeric_out, + PointerGetDatum(v.val.numeric))); + else if (v.type == jbvNull) + return pstrdup("null"); + else + { + elog(ERROR, "unrecognized jsonb value type %d", v.type); + return NULL; + } + } + else + return JsonbToCString(NULL, &jb->root, VARSIZE(jb)); +} diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index 612bbf06a3..fca72de558 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -265,6 +265,7 @@ typedef struct PopulateArrayContext int *dims; /* dimensions */ int *sizes; /* current dimension counters */ int ndims; /* number of dimensions */ + Node *escontext; /* For soft-error capture */ } PopulateArrayContext; /* state for populate_array_json() */ @@ -442,12 +443,13 @@ static void JsValueToJsObject(JsValue *jsv, JsObject *jso); static Datum populate_composite(CompositeIOData *io, Oid typid, const char *colname, MemoryContext mcxt, HeapTupleHeader defaultval, JsValue *jsv, bool isnull); -static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv); +static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext); static void prepare_column_cache(ColumnIOData *column, Oid typid, int32 typmod, MemoryContext mcxt, bool need_scalar); static Datum populate_record_field(ColumnIOData *col, Oid typid, int32 typmod, const char *colname, MemoryContext mcxt, Datum defaultval, - JsValue *jsv, bool *isnull); + JsValue *jsv, bool *isnull, Node *escontext); static RecordIOData *allocate_record_info(MemoryContext mcxt, int ncolumns); static bool JsObjectGetField(JsObject *obj, char *field, JsValue *jsv); static void populate_recordset_record(PopulateRecordsetState *state, JsObject *obj); @@ -459,7 +461,8 @@ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims); static void populate_array_check_dimension(PopulateArrayContext *ctx, int ndim); static void populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv); static Datum populate_array(ArrayIOData *aio, const char *colname, - MemoryContext mcxt, JsValue *jsv); + MemoryContext mcxt, JsValue *jsv, Node *escontext, + bool *isnull); static Datum populate_domain(DomainIOData *io, Oid typid, const char *colname, MemoryContext mcxt, JsValue *jsv, bool isnull); @@ -2484,12 +2487,12 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) if (ndim <= 0) { if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the value of key \"%s\".", ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"))); } @@ -2506,13 +2509,13 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) appendStringInfo(&indices, "[%d]", ctx->sizes[i]); if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s of key \"%s\".", indices.data, ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s.", @@ -2520,7 +2523,11 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) } } -/* set the number of dimensions of the populated array when it becomes known */ +/* + * Set the number of dimensions of the populated array when it becomes known. + * + * Returns without doing anything if the input (ndims) is erratic. + */ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) { @@ -2531,6 +2538,10 @@ populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) if (ndims <= 0) populate_array_report_expected_array(ctx, ndims); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + ctx->ndims = ndims; ctx->dims = palloc(sizeof(int) * ndims); ctx->sizes = palloc0(sizeof(int) * ndims); @@ -2548,12 +2559,16 @@ populate_array_check_dimension(PopulateArrayContext *ctx, int ndim) if (ctx->dims[ndim] == -1) ctx->dims[ndim] = dim; /* assign dimension if not yet known */ else if (ctx->dims[ndim] != dim) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("malformed JSON array"), errdetail("Multidimensional arrays must have " "sub-arrays with matching dimensions."))); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + /* reset the current array dimension size counter */ ctx->sizes[ndim] = 0; @@ -2573,7 +2588,10 @@ populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv) ctx->aio->element_type, ctx->aio->element_typmod, NULL, ctx->mcxt, PointerGetDatum(NULL), - jsv, &element_isnull); + jsv, &element_isnull, ctx->escontext); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; accumArrayResult(ctx->astate, element, element_isnull, ctx->aio->element_type, ctx->acxt); @@ -2594,6 +2612,10 @@ populate_array_object_start(void *_state) else if (ndim < state->ctx->ndims) populate_array_report_expected_array(state->ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2611,6 +2633,10 @@ populate_array_array_end(void *_state) if (ndim < ctx->ndims) populate_array_check_dimension(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2686,6 +2712,10 @@ populate_array_scalar(void *_state, char *token, JsonTokenType tokentype) else if (ndim < ctx->ndims) populate_array_report_expected_array(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + if (ndim == ctx->ndims) { /* remember the scalar element token */ @@ -2715,7 +2745,13 @@ populate_array_json(PopulateArrayContext *ctx, char *json, int len) sem.array_element_end = populate_array_element_end; sem.scalar = populate_array_scalar; - pg_parse_json_or_ereport(state.lex, &sem); + if (!pg_parse_json_or_errsave(state.lex, &sem, ctx->escontext)) + { + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + pfree(state.lex); + return; + } /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2740,10 +2776,15 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ check_stack_depth(); - if (jbv->type != jbvBinary || !JsonContainerIsArray(jbc)) + if (jbv->type != jbvBinary || + !JsonContainerIsArray(jbc) || + JsonContainerIsScalar(jbc)) + { populate_array_report_expected_array(ctx, ndim - 1); - - Assert(!JsonContainerIsScalar(jbc)); + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + return; + } it = JsonbIteratorInit(jbc); @@ -2762,7 +2803,12 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ (tok == WJB_ELEM && (val.type != jbvBinary || !JsonContainerIsArray(val.val.binary.data))))) + { populate_array_assign_ndims(ctx, ndim); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + } jsv.is_json = false; jsv.val.jsonb = &val; @@ -2780,6 +2826,9 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ { /* populate child sub-array */ populate_array_dim_jsonb(ctx, &val, ndim + 1); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2797,12 +2846,18 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ Assert(tok == WJB_DONE && !it); } -/* recursively populate an array from json/jsonb */ +/* + * Recursively populate an array from json/jsonb + * + * *isnull is set to true if an error is reported during parsing. + */ static Datum populate_array(ArrayIOData *aio, const char *colname, MemoryContext mcxt, - JsValue *jsv) + JsValue *jsv, + Node *escontext, + bool *isnull) { PopulateArrayContext ctx; Datum result; @@ -2817,6 +2872,7 @@ populate_array(ArrayIOData *aio, ctx.ndims = 0; /* unknown yet */ ctx.dims = NULL; ctx.sizes = NULL; + ctx.escontext = escontext; if (jsv->is_json) populate_array_json(&ctx, jsv->val.json.str, @@ -2825,7 +2881,16 @@ populate_array(ArrayIOData *aio, else { populate_array_dim_jsonb(&ctx, jsv->val.jsonb, 1); - ctx.dims[0] = ctx.sizes[0]; + /* Nothing to do on an error. */ + if (!SOFT_ERROR_OCCURRED(ctx.escontext)) + ctx.dims[0] = ctx.sizes[0]; + } + + /* Nothing to return if an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx.escontext)) + { + *isnull = true; + return (Datum) 0; } Assert(ctx.ndims > 0); @@ -2842,6 +2907,7 @@ populate_array(ArrayIOData *aio, pfree(ctx.sizes); pfree(lbs); + *isnull = false; return result; } @@ -2957,7 +3023,8 @@ populate_composite(CompositeIOData *io, /* populate non-null scalar value from json/jsonb value */ static Datum -populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) +populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext) { Datum res; char *str = NULL; @@ -3028,7 +3095,11 @@ populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) elog(ERROR, "unrecognized jsonb type: %d", (int) jbv->type); } - res = InputFunctionCall(&io->typiofunc, str, io->typioparam, typmod); + if (!InputFunctionCallSafe(&io->typiofunc, str, io->typioparam, typmod, + escontext, &res)) + { + res = (Datum) 0; + } /* free temporary buffer */ if (str != json) @@ -3054,7 +3125,7 @@ populate_domain(DomainIOData *io, res = populate_record_field(io->base_io, io->base_typid, io->base_typmod, colname, mcxt, PointerGetDatum(NULL), - jsv, &isnull); + jsv, &isnull, NULL); Assert(!isnull); } @@ -3159,7 +3230,8 @@ populate_record_field(ColumnIOData *col, MemoryContext mcxt, Datum defaultval, JsValue *jsv, - bool *isnull) + bool *isnull, + Node *escontext) { TypeCat typcat; @@ -3192,10 +3264,12 @@ populate_record_field(ColumnIOData *col, switch (typcat) { case TYPECAT_SCALAR: - return populate_scalar(&col->scalar_io, typid, typmod, jsv); + return populate_scalar(&col->scalar_io, typid, typmod, jsv, + escontext); case TYPECAT_ARRAY: - return populate_array(&col->io.array, colname, mcxt, jsv); + return populate_array(&col->io.array, colname, mcxt, jsv, + escontext, isnull); case TYPECAT_COMPOSITE: case TYPECAT_COMPOSITE_DOMAIN: @@ -3216,6 +3290,53 @@ populate_record_field(ColumnIOData *col, } } +/* recursively populate specified type from a json/jsonb value */ +Datum +json_populate_type(Datum json_val, Oid json_type, Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext) +{ + JsValue jsv = {0}; + JsonbValue jbv; + + jsv.is_json = json_type == JSONOID; + + if (*isnull) + { + if (jsv.is_json) + jsv.val.json.str = NULL; + else + jsv.val.jsonb = NULL; + } + else if (jsv.is_json) + { + text *json = DatumGetTextPP(json_val); + + jsv.val.json.str = VARDATA_ANY(json); + jsv.val.json.len = VARSIZE_ANY_EXHDR(json); + jsv.val.json.type = JSON_TOKEN_INVALID; /* not used in + * populate_composite() */ + } + else + { + Jsonb *jsonb = DatumGetJsonbP(json_val); + + jsv.val.jsonb = &jbv; + + /* fill binary jsonb value pointing to jb */ + jbv.type = jbvBinary; + jbv.val.binary.data = &jsonb->root; + jbv.val.binary.len = VARSIZE(jsonb) - VARHDRSZ; + } + + if (!*cache) + *cache = MemoryContextAllocZero(mcxt, sizeof(ColumnIOData)); + + return populate_record_field(*cache, typid, typmod, NULL, mcxt, + PointerGetDatum(NULL), &jsv, isnull, + escontext); +} + static RecordIOData * allocate_record_info(MemoryContext mcxt, int ncolumns) { @@ -3357,7 +3478,8 @@ populate_record(TupleDesc tupdesc, mcxt, nulls[i] ? (Datum) 0 : values[i], &field, - &nulls[i]); + &nulls[i], + NULL); } res = heap_form_tuple(tupdesc, values, nulls); diff --git a/src/backend/utils/adt/jsonpath.c b/src/backend/utils/adt/jsonpath.c index 7891fde310..5194d0d91f 100644 --- a/src/backend/utils/adt/jsonpath.c +++ b/src/backend/utils/adt/jsonpath.c @@ -68,7 +68,9 @@ #include "libpq/pqformat.h" #include "nodes/miscnodes.h" #include "miscadmin.h" +#include "nodes/nodeFuncs.h" #include "utils/builtins.h" +#include "utils/formatting.h" #include "utils/json.h" #include "utils/jsonpath.h" @@ -1109,3 +1111,256 @@ jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, return true; } + +/* SQL/JSON datatype status: */ +typedef enum JsonPathDatatypeStatus +{ + jpdsNonDateTime, /* null, bool, numeric, string, array, object */ + jpdsUnknownDateTime, /* unknown datetime type */ + jpdsDateTimeZoned, /* timetz, timestamptz */ + jpdsDateTimeNonZoned /* time, timestamp, date */ +} JsonPathDatatypeStatus; + +/* Context for jspIsMutableWalker() */ +typedef struct JsonPathMutableContext +{ + List *varnames; /* list of variable names */ + List *varexprs; /* list of variable expressions */ + JsonPathDatatypeStatus current; /* status of @ item */ + bool lax; /* jsonpath is lax or strict */ + bool mutable; /* resulting mutability status */ +} JsonPathMutableContext; + +/* + * Recursive walker for jspIsMutable() + */ +static JsonPathDatatypeStatus +jspIsMutableWalker(JsonPathItem *jpi, JsonPathMutableContext *cxt) +{ + JsonPathItem next; + JsonPathDatatypeStatus status = jpdsNonDateTime; + + while (!cxt->mutable) + { + JsonPathItem arg; + JsonPathDatatypeStatus leftStatus; + JsonPathDatatypeStatus rightStatus; + + switch (jpi->type) + { + case jpiRoot: + Assert(status == jpdsNonDateTime); + break; + + case jpiCurrent: + Assert(status == jpdsNonDateTime); + status = cxt->current; + break; + + case jpiFilter: + { + JsonPathDatatypeStatus prevStatus = cxt->current; + + cxt->current = status; + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + + cxt->current = prevStatus; + break; + } + + case jpiVariable: + { + int32 len; + const char *name = jspGetString(jpi, &len); + ListCell *lc1; + ListCell *lc2; + + Assert(status == jpdsNonDateTime); + + forboth(lc1, cxt->varnames, lc2, cxt->varexprs) + { + String *varname = lfirst_node(String, lc1); + Node *varexpr = lfirst(lc2); + + if (strncmp(varname->sval, name, len)) + continue; + + switch (exprType(varexpr)) + { + case DATEOID: + case TIMEOID: + case TIMESTAMPOID: + status = jpdsDateTimeNonZoned; + break; + + case TIMETZOID: + case TIMESTAMPTZOID: + status = jpdsDateTimeZoned; + break; + + default: + status = jpdsNonDateTime; + break; + } + + break; + } + break; + } + + case jpiEqual: + case jpiNotEqual: + case jpiLess: + case jpiGreater: + case jpiLessOrEqual: + case jpiGreaterOrEqual: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + leftStatus = jspIsMutableWalker(&arg, cxt); + + jspGetRightArg(jpi, &arg); + rightStatus = jspIsMutableWalker(&arg, cxt); + + /* + * Comparison of datetime type with different timezone status + * is mutable. + */ + if (leftStatus != jpdsNonDateTime && + rightStatus != jpdsNonDateTime && + (leftStatus == jpdsUnknownDateTime || + rightStatus == jpdsUnknownDateTime || + leftStatus != rightStatus)) + cxt->mutable = true; + break; + + case jpiNot: + case jpiIsUnknown: + case jpiExists: + case jpiPlus: + case jpiMinus: + Assert(status == jpdsNonDateTime); + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiAnd: + case jpiOr: + case jpiAdd: + case jpiSub: + case jpiMul: + case jpiDiv: + case jpiMod: + case jpiStartsWith: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + jspGetRightArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiIndexArray: + for (int i = 0; i < jpi->content.array.nelems; i++) + { + JsonPathItem from; + JsonPathItem to; + + if (jspGetArraySubscript(jpi, &from, &to, i)) + jspIsMutableWalker(&to, cxt); + + jspIsMutableWalker(&from, cxt); + } + /* FALLTHROUGH */ + + case jpiAnyArray: + if (!cxt->lax) + status = jpdsNonDateTime; + break; + + case jpiAny: + if (jpi->content.anybounds.first > 0) + status = jpdsNonDateTime; + break; + + case jpiDatetime: + if (jpi->content.arg) + { + char *template; + + jspGetArg(jpi, &arg); + if (arg.type != jpiString) + { + status = jpdsNonDateTime; + break; /* there will be runtime error */ + } + + template = jspGetString(&arg, NULL); + if (datetime_format_has_tz(template)) + status = jpdsDateTimeZoned; + else + status = jpdsDateTimeNonZoned; + } + else + { + status = jpdsUnknownDateTime; + } + break; + + case jpiLikeRegex: + Assert(status == jpdsNonDateTime); + jspInitByBuffer(&arg, jpi->base, jpi->content.like_regex.expr); + jspIsMutableWalker(&arg, cxt); + break; + + /* literals */ + case jpiNull: + case jpiString: + case jpiNumeric: + case jpiBool: + /* accessors */ + case jpiKey: + case jpiAnyKey: + /* special items */ + case jpiSubscript: + case jpiLast: + /* item methods */ + case jpiType: + case jpiSize: + case jpiAbs: + case jpiFloor: + case jpiCeiling: + case jpiDouble: + case jpiKeyValue: + status = jpdsNonDateTime; + break; + } + + if (!jspGetNext(jpi, &next)) + break; + + jpi = &next; + } + + return status; +} + +/* + * Check whether jsonpath expression is immutable or not. + */ +bool +jspIsMutable(JsonPath *path, List *varnames, List *varexprs) +{ + JsonPathMutableContext cxt; + JsonPathItem jpi; + + cxt.varnames = varnames; + cxt.varexprs = varexprs; + cxt.current = jpdsNonDateTime; + cxt.lax = (path->header & JSONPATH_LAX) != 0; + cxt.mutable = false; + + jspInit(&jpi, path); + jspIsMutableWalker(&jpi, &cxt); + + return cxt.mutable; +} diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index 2d0599b4aa..eded22e194 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -87,12 +87,16 @@ typedef struct JsonBaseObjectInfo int id; } JsonBaseObjectInfo; +typedef int (*JsonPathVarCallback) (void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); + /* * Context of jsonpath execution. */ typedef struct JsonPathExecContext { - Jsonb *vars; /* variables to substitute into jsonpath */ + void *vars; /* variables to substitute into jsonpath */ + JsonPathVarCallback getVar; JsonbValue *root; /* for $ evaluation */ JsonbValue *current; /* for @ evaluation */ JsonBaseObjectInfo baseObject; /* "base object" for .keyvalue() @@ -174,7 +178,8 @@ typedef JsonPathBool (*JsonPathPredicateCallback) (JsonPathItem *jsp, void *param); typedef Numeric (*BinaryArithmFunc) (Numeric num1, Numeric num2, bool *error); -static JsonPathExecResult executeJsonPath(JsonPath *path, Jsonb *vars, +static JsonPathExecResult executeJsonPath(JsonPath *path, void *vars, + JsonPathVarCallback getVar, Jsonb *json, bool throwErrors, JsonValueList *result, bool useTz); static JsonPathExecResult executeItem(JsonPathExecContext *cxt, @@ -225,8 +230,13 @@ static JsonPathExecResult appendBoolResult(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonValueList *found, JsonPathBool res); static void getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, JsonbValue *value); +static int GetJsonPathVar(void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); static void getJsonPathVariable(JsonPathExecContext *cxt, - JsonPathItem *variable, Jsonb *vars, JsonbValue *value); + JsonPathItem *variable, JsonbValue *value); +static int getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, + int varNameLen, JsonbValue *val, + JsonbValue *baseObject); static int JsonbArraySize(JsonbValue *jb); static JsonPathBool executeComparison(JsonPathItem *cmp, JsonbValue *lv, JsonbValue *rv, void *p); @@ -284,7 +294,8 @@ jsonb_path_exists_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - res = executeJsonPath(jp, vars, jb, !silent, NULL, tz); + res = executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, NULL, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -339,7 +350,8 @@ jsonb_path_match_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -417,7 +429,8 @@ jsonb_path_query_internal(FunctionCallInfo fcinfo, bool tz) vars = PG_GETARG_JSONB_P_COPY(2); silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); funcctx->user_fctx = JsonValueListGetList(&found); @@ -464,7 +477,8 @@ jsonb_path_query_array_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_RETURN_JSONB_P(JsonbValueToJsonb(wrapItemsInArray(&found))); } @@ -495,7 +509,8 @@ jsonb_path_query_first_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); if (JsonValueListLength(&found) >= 1) PG_RETURN_JSONB_P(JsonbValueToJsonb(JsonValueListHead(&found))); @@ -537,8 +552,9 @@ jsonb_path_query_first_tz(PG_FUNCTION_ARGS) * In other case it tries to find all the satisfied result items. */ static JsonPathExecResult -executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, - JsonValueList *result, bool useTz) +executeJsonPath(JsonPath *path, void *vars, JsonPathVarCallback getVar, + Jsonb *json, bool throwErrors, JsonValueList *result, + bool useTz) { JsonPathExecContext cxt; JsonPathExecResult res; @@ -550,22 +566,16 @@ executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, if (!JsonbExtractScalar(&json->root, &jbv)) JsonbInitBinary(&jbv, json); - if (vars && !JsonContainerIsObject(&vars->root)) - { - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"vars\" argument is not an object"), - errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); - } - cxt.vars = vars; + cxt.getVar = getVar; cxt.laxMode = (path->header & JSONPATH_LAX) != 0; cxt.ignoreStructuralErrors = cxt.laxMode; cxt.root = &jbv; cxt.current = &jbv; cxt.baseObject.jbc = NULL; cxt.baseObject.id = 0; - cxt.lastGeneratedObjectId = vars ? 2 : 1; + /* 1 + number of base objects in vars */ + cxt.lastGeneratedObjectId = 1 + getVar(vars, NULL, 0, NULL, NULL); cxt.innermostArraySize = -1; cxt.throwErrors = throwErrors; cxt.useTz = useTz; @@ -2108,54 +2118,118 @@ getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, &value->val.string.len); break; case jpiVariable: - getJsonPathVariable(cxt, item, cxt->vars, value); + getJsonPathVariable(cxt, item, value); return; default: elog(ERROR, "unexpected jsonpath item type"); } } +/* + * Returns the computed value of a JSON path variable with given name. + */ +static int +GetJsonPathVar(void *cxt, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject) +{ + JsonPathVariable *var = NULL; + List *vars = cxt; + ListCell *lc; + int id = 1; + + if (!varName) + return list_length(vars); + + foreach(lc, vars) + { + JsonPathVariable *curvar = lfirst(lc); + + if (!strncmp(curvar->name, varName, varNameLen)) + { + var = curvar; + break; + } + + id++; + } + + if (!var) + return -1; + + if (var->isnull) + { + val->type = jbvNull; + return 0; + } + + JsonItemFromDatum(var->value, var->typid, var->typmod, val); + + *baseObject = *val; + return id; +} + /* * Get the value of variable passed to jsonpath executor */ static void getJsonPathVariable(JsonPathExecContext *cxt, JsonPathItem *variable, - Jsonb *vars, JsonbValue *value) + JsonbValue *value) { char *varName; int varNameLength; + JsonbValue baseObject; + int baseObjectId; + + Assert(variable->type == jpiVariable); + varName = jspGetString(variable, &varNameLength); + + if (!cxt->vars || + (baseObjectId = cxt->getVar(cxt->vars, varName, varNameLength, value, + &baseObject)) < 0) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("could not find jsonpath variable \"%s\"", + pnstrdup(varName, varNameLength)))); + + if (baseObjectId > 0) + setBaseObject(cxt, &baseObject, baseObjectId); +} + +static int +getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, int varNameLength, + JsonbValue *value, JsonbValue *baseObject) +{ + Jsonb *vars = varsJsonb; JsonbValue tmp; JsonbValue *v; - if (!vars) + if (!varName) { - value->type = jbvNull; - return; + if (vars && !JsonContainerIsObject(&vars->root)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"vars\" argument is not an object"), + errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); + } + + return vars ? 1 : 0; /* count of base objects */ } - Assert(variable->type == jpiVariable); - varName = jspGetString(variable, &varNameLength); tmp.type = jbvString; tmp.val.string.val = varName; tmp.val.string.len = varNameLength; v = findJsonbValueFromContainer(&vars->root, JB_FOBJECT, &tmp); - if (v) - { - *value = *v; - pfree(v); - } - else - { - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("could not find jsonpath variable \"%s\"", - pnstrdup(varName, varNameLength)))); - } + if (!v) + return -1; - JsonbInitBinary(&tmp, vars); - setBaseObject(cxt, &tmp, 1); + *value = *v; + pfree(v); + + JsonbInitBinary(baseObject, vars); + return 1; } /**************** Support functions for JsonPath execution *****************/ @@ -2812,3 +2886,240 @@ compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, return DatumGetInt32(DirectFunctionCall2(cmpfunc, val1, val2)); } + +/* Executor-callable JSON_EXISTS implementation */ +bool +JsonPathExists(Datum jb, JsonPath *jp, List *vars, bool *error) +{ + JsonPathExecResult res = executeJsonPath(jp, vars, GetJsonPathVar, + DatumGetJsonbP(jb), !error, NULL, + true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + *error = true; + + return res == jperOk; +} + +/* Executor-callable JSON_QUERY implementation */ +Datum +JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, bool *empty, + bool *error, List *vars) +{ + JsonbValue *first; + bool wrap; + JsonValueList found = {0}; + JsonPathExecResult res PG_USED_FOR_ASSERTS_ONLY; + int count; + + res = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + { + *error = true; + *empty = false; + return (Datum) 0; + } + + count = JsonValueListLength(&found); + + first = count ? JsonValueListHead(&found) : NULL; + + if (!first) + wrap = false; + else if (wrapper == JSW_NONE) + wrap = false; + else if (wrapper == JSW_UNCONDITIONAL) + wrap = true; + else if (wrapper == JSW_CONDITIONAL) + wrap = count > 1 || + IsAJsonbScalar(first) || + (first->type == jbvBinary && + JsonContainerIsScalar(first->val.binary.data)); + else + { + elog(ERROR, "unrecognized json wrapper %d", wrapper); + wrap = false; + } + + if (wrap) + return JsonbPGetDatum(JsonbValueToJsonb(wrapItemsInArray(&found))); + + if (count > 1) + { + if (error) + { + *error = true; + return (Datum) 0; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_QUERY should return singleton item without wrapper"), + errhint("Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array."))); + } + + if (first) + return JsonbPGetDatum(JsonbValueToJsonb(first)); + + *empty = true; + return PointerGetDatum(NULL); +} + +/* Executor-callable JSON_VALUE implementation */ +JsonbValue * +JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars) +{ + JsonbValue *res; + JsonValueList found = {0}; + JsonPathExecResult jper PG_USED_FOR_ASSERTS_ONLY; + int count; + + jper = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(jper)); + + if (error && jperIsError(jper)) + { + *error = true; + *empty = false; + return NULL; + } + + count = JsonValueListLength(&found); + + *empty = !count; + + if (*empty) + return NULL; + + if (count > 1) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + res = JsonValueListHead(&found); + + if (res->type == jbvBinary && + JsonContainerIsScalar(res->val.binary.data)) + JsonbExtractScalar(res->val.binary.data, res); + + if (!IsAJsonbScalar(res)) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_SCALAR_REQUIRED), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + if (res->type == jbvNull) + return NULL; + + return res; +} + +static void +JsonbValueInitNumericDatum(JsonbValue *jbv, Datum num) +{ + jbv->type = jbvNumeric; + jbv->val.numeric = DatumGetNumeric(num); +} + +void +JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) +{ + switch (typid) + { + case BOOLOID: + res->type = jbvBool; + res->val.boolean = DatumGetBool(val); + break; + case NUMERICOID: + JsonbValueInitNumericDatum(res, val); + break; + case INT2OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int2_numeric, val)); + break; + case INT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int4_numeric, val)); + break; + case INT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int8_numeric, val)); + break; + case FLOAT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float4_numeric, val)); + break; + case FLOAT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float8_numeric, val)); + break; + case TEXTOID: + case VARCHAROID: + res->type = jbvString; + res->val.string.val = VARDATA_ANY(val); + res->val.string.len = VARSIZE_ANY_EXHDR(val); + break; + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + res->type = jbvDatetime; + res->val.datetime.value = val; + res->val.datetime.typid = typid; + res->val.datetime.typmod = typmod; + res->val.datetime.tz = 0; + break; + case JSONBOID: + { + JsonbValue *jbv = res; + Jsonb *jb = DatumGetJsonbP(val); + + if (JsonContainerIsScalar(&jb->root)) + { + bool result PG_USED_FOR_ASSERTS_ONLY; + + result = JsonbExtractScalar(&jb->root, jbv); + Assert(result); + } + else + JsonbInitBinary(jbv, jb); + break; + } + case JSONOID: + { + text *txt = DatumGetTextP(val); + char *str = text_to_cstring(txt); + Jsonb *jb; + + jb = DatumGetJsonbP(DirectFunctionCall1(jsonb_in, + CStringGetDatum(str))); + pfree(str); + + JsonItemFromDatum(JsonbPGetDatum(jb), JSONBOID, -1, res); + break; + } + default: + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); + } +} diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d1b03d6cb2..d1ec418ccf 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -476,6 +476,8 @@ static void get_const_expr(Const *constval, deparse_context *context, int showtype); static void get_const_collation(Const *constval, deparse_context *context); static void get_json_format(JsonFormat *format, StringInfo buf); +static void get_json_returning(JsonReturning *returning, StringInfo buf, + bool json_format_by_default); static void get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, bool showimplicit); static void get_json_constructor_options(JsonConstructorExpr *ctor, @@ -518,6 +520,8 @@ static char *generate_qualified_type_name(Oid typid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); +static void get_json_path_spec(Node *path_spec, deparse_context *context, + bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8279,6 +8283,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_WindowFunc: case T_FuncExpr: case T_JsonConstructorExpr: + case T_JsonExpr: /* function-like: name(..) or name[..] */ return true; @@ -8450,6 +8455,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_GroupingFunc: /* own parentheses */ case T_WindowFunc: /* own parentheses */ case T_CaseExpr: /* other separators */ + case T_JsonExpr: /* own parentheses */ return true; default: return false; @@ -8565,6 +8571,65 @@ get_rule_expr_paren(Node *node, deparse_context *context, appendStringInfoChar(context->buf, ')'); } +static void +get_json_behavior(JsonBehavior *behavior, deparse_context *context, + const char *on) +{ + /* + * The order of array elements must correspond to the order of + * JsonBehaviorType members. + */ + const char *behavior_names[] = + { + " NULL", + " ERROR", + " EMPTY", + " TRUE", + " FALSE", + " UNKNOWN", + " EMPTY ARRAY", + " EMPTY OBJECT", + " DEFAULT " + }; + + if ((int) behavior->btype < 0 || behavior->btype >= lengthof(behavior_names)) + elog(ERROR, "invalid json behavior type: %d", behavior->btype); + + appendStringInfoString(context->buf, behavior_names[behavior->btype]); + + if (behavior->btype == JSON_BEHAVIOR_DEFAULT) + get_rule_expr(behavior->default_expr, context, false); + + appendStringInfo(context->buf, " ON %s", on); +} + +/* + * get_json_expr_options + * + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + */ +static void +get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, + JsonBehaviorType default_behavior) +{ + if (jsexpr->op == JSON_QUERY_OP) + { + if (jsexpr->wrapper == JSW_CONDITIONAL) + appendStringInfo(context->buf, " WITH CONDITIONAL WRAPPER"); + else if (jsexpr->wrapper == JSW_UNCONDITIONAL) + appendStringInfo(context->buf, " WITH UNCONDITIONAL WRAPPER"); + + if (jsexpr->omit_quotes) + appendStringInfo(context->buf, " OMIT QUOTES"); + } + + if (jsexpr->op != JSON_EXISTS_OP && + jsexpr->on_empty->btype != default_behavior) + get_json_behavior(jsexpr->on_empty, context, "EMPTY"); + + if (jsexpr->on_error->btype != default_behavior) + get_json_behavior(jsexpr->on_error, context, "ERROR"); +} /* ---------- * get_rule_expr - Parse back an expression @@ -9724,6 +9789,7 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonValueExpr: { JsonValueExpr *jve = (JsonValueExpr *) node; @@ -9773,6 +9839,63 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + appendStringInfoString(buf, "JSON_QUERY("); + break; + case JSON_VALUE_OP: + appendStringInfoString(buf, "JSON_VALUE("); + break; + case JSON_EXISTS_OP: + appendStringInfoString(buf, "JSON_EXISTS("); + break; + } + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_json_path_spec(jexpr->path_spec, context, showimplicit); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoString(buf, " PASSING "); + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + get_rule_expr((Node *) lfirst(lc2), context, showimplicit); + appendStringInfo(buf, " AS %s", + ((String *) lfirst_node(String, lc1))->sval); + } + } + + if (jexpr->op != JSON_EXISTS_OP || + jexpr->returning->typid != BOOLOID) + get_json_returning(jexpr->returning, context->buf, + jexpr->op == JSON_QUERY_OP); + + get_json_expr_options(jexpr, context, + jexpr->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : JSON_BEHAVIOR_NULL); + + appendStringInfoString(buf, ")"); + } + break; + case T_List: { char *sep; @@ -9896,6 +10019,7 @@ looks_like_function(Node *node) case T_MinMaxExpr: case T_SQLValueFunction: case T_XmlExpr: + case T_JsonExpr: /* these are all accepted by func_expr_common_subexpr */ return true; default: @@ -10755,6 +10879,18 @@ get_const_collation(Const *constval, deparse_context *context) } } +/* + * get_json_path_spec - Parse back a JSON path specification + */ +static void +get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit) +{ + if (IsA(path_spec, Const)) + get_const_expr((Const *) path_spec, context, -1); + else + get_rule_expr(path_spec, context, showimplicit); +} + /* * get_json_format - Parse back a JsonFormat node */ diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index 048573c2bc..69fb577850 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -16,12 +16,15 @@ #include "executor/nodeAgg.h" #include "nodes/execnodes.h" +#include "nodes/miscnodes.h" /* forward references to avoid circularity */ struct ExprEvalStep; struct SubscriptingRefState; struct ScalarArrayOpExprHashTable; struct JsonConstructorExprState; +struct JsonbValue; +struct JsonExprState; /* Bits in ExprState->flags (see also execnodes.h for public flag bits): */ /* expression's interpreter has been initialized */ @@ -238,6 +241,11 @@ typedef enum ExprEvalOp EEOP_XMLEXPR, EEOP_JSON_CONSTRUCTOR, EEOP_IS_JSON, + EEOP_JSONEXPR_SKIP, + EEOP_JSONEXPR_PATH, + EEOP_JSONEXPR_BEHAVIOR, + EEOP_JSONEXPR_COERCION, + EEOP_JSONEXPR_COERCION_FINISH, EEOP_AGGREF, EEOP_GROUPING_FUNC, EEOP_WINDOW_FUNC, @@ -689,6 +697,57 @@ typedef struct ExprEvalStep JsonIsPredicate *pred; /* original expression node */ } is_json; + /* for EEOP_JSONEXPR_PATH */ + struct + { + struct JsonExprState *jsestate; + } jsonexpr; + + /* for EEOP_JSONEXPR_SKIP */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprSkip() */ + int jump_coercion; + int jump_passing_args; + } jsonexpr_skip; + + /* for EEOP_JSONEXPR_BEHAVIOR */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprBehavior() */ + int jump_onerror_expr; + int jump_onempty_expr; + int jump_coercion; + int jump_skip_coercion; + } jsonexpr_behavior; + + /* for EEOP_JSONEXPR_COERCION */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion; + + /* for EEOP_JSONEXPR_COERCION_FINISH */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion_finish; } d; } ExprEvalStep; @@ -752,6 +811,111 @@ typedef struct JsonConstructorExprState int nargs; } JsonConstructorExprState; +/* + * Information computed before evaluating a JsonExpr expression. + */ +typedef struct JsonExprPreEvalState +{ + /* value/isnull for JsonExpr.formatted_expr */ + NullableDatum formatted_expr; + + /* value/isnull for JsonExpr.pathspec */ + NullableDatum pathspec; + + /* JsonPathVariable entries for JsonExpr.passing_values */ + List *args; +} JsonExprPreEvalState; + +/* + * State for a given member of JsonItemCoercions. + */ +typedef struct JsonCoercionState +{ + /* Expression used to evaluate the coercion */ + JsonCoercion *coercion; + + /* ExprEvalStep to compute this coercion's expression */ + int jump_eval_expr; +} JsonCoercionState; + +/* + * State for evaluating the coercion for a given JSON item using one of + * the following coercions. + * + * Note that while ExecInitExprRec() for JsonItemCoercions will initialize + * ExprEvalSteps for all of the members that need it, only one will get run + * during a given evaluation of the enclosing JsonExpr depending on the type + * of the result JSON item. + */ +typedef struct JsonItemCoercionsState +{ + JsonCoercionState *null; + JsonCoercionState *string; + JsonCoercionState *numeric; + JsonCoercionState *boolean; + JsonCoercionState *date; + JsonCoercionState *time; + JsonCoercionState *timetz; + JsonCoercionState *timestamp; + JsonCoercionState *timestamptz; + JsonCoercionState *composite; +} JsonItemCoercionsState; + +/* + * State for some post-JsonExpr-evaluation processing steps that gets filled + * in JsonExpr evaluation. + */ +typedef struct JsonExprPostEvalState +{ + /* Is JSON item empty? */ + bool empty; + + /* Did JSON item evaluation cause an error? */ + bool error; + + /* Cache for json_populate_type() called for coercion in some cases */ + void *cache; + + /* + * State for evaluating a JSON item coercion. Points to one of those in + * JsonExprState.coercions chosen by ExecPrepareJsonItemCoercion(). + */ + JsonCoercionState *item_jcstate; + bool coercion_error; + bool coercion_done; + + ErrorSaveContext escontext; +} JsonExprPostEvalState; + +/* EEOP_JSONEXPR state, too big to inline */ +typedef struct JsonExprState +{ + /* original expression node */ + JsonExpr *jsexpr; + + /* + * Should errors be thrown or handled softly? Different parts of + * evaluating a given JsonExpr may require different treatment depending + * on the JsonExprOp; see ExecEvalJsonExprBehavior(). + */ + bool throw_error; + + JsonExprPreEvalState pre_eval; + JsonExprPostEvalState post_eval; + + struct + { + FmgrInfo *finfo; /* typinput function for output type */ + Oid typioparam; + } input; /* I/O info for output type */ + + /* + * Either of the following two is used by ExecEvalJsonExprCoercion() to + * apply coercion to the final result if needed. + */ + JsonCoercionState *result_jcstate; + JsonItemCoercionsState *item_jcstates; +} JsonExprState; /* functions in execExpr.c */ extern void ExprEvalPushStep(ExprState *es, const ExprEvalStep *s); @@ -805,6 +969,14 @@ extern void ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op); extern void ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull); +extern int ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op); +extern void ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, + ExprContext *econtext); extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op); extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op, ExprContext *econtext); diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index ac02247947..089d1c5750 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -17,6 +17,7 @@ #include "executor/execdesc.h" #include "fmgr.h" #include "nodes/lockoptions.h" +#include "nodes/miscnodes.h" #include "nodes/parsenodes.h" #include "utils/memutils.h" diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index cb714f4a19..584bf7001a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -129,6 +129,9 @@ typedef struct ExprState Datum *innermost_domainval; bool *innermost_domainnull; + + /* ErrorSaveContext for soft-error capture. */ + Node *escontext; } ExprState; diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 06d991b725..5e149b0266 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -111,6 +111,7 @@ extern VacuumRelation *makeVacuumRelation(RangeVar *relation, Oid oid, List *va_ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *expr, JsonFormat *format); +extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index d926713bd9..7d63a37d5f 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1727,6 +1727,12 @@ typedef enum JsonQuotes JS_QUOTES_OMIT /* OMIT QUOTES */ } JsonQuotes; +/* + * JsonPathSpec - + * representation of JSON path constant + */ +typedef char *JsonPathSpec; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1738,6 +1744,48 @@ typedef struct JsonOutput JsonReturning *returning; /* RETURNING FORMAT clause and type Oids */ } JsonOutput; +/* + * JsonArgument - + * representation of argument from JSON PASSING clause + */ +typedef struct JsonArgument +{ + NodeTag type; + JsonValueExpr *val; /* argument value expression */ + char *name; /* argument name */ +} JsonArgument; + +/* + * JsonCommon - + * representation of common syntax of functions using JSON path + */ +typedef struct JsonCommon +{ + NodeTag type; + JsonValueExpr *expr; /* context item expression */ + Node *pathspec; /* JSON path specification expression */ + char *pathname; /* path name, if any */ + List *passing; /* list of PASSING clause arguments, if any */ + int location; /* token location, or -1 if unknown */ +} JsonCommon; + +/* + * JsonFuncExpr - + * untransformed representation of JSON function expressions + */ +typedef struct JsonFuncExpr +{ + NodeTag type; + JsonExprOp op; /* expression type */ + JsonCommon *common; /* common syntax */ + JsonOutput *output; /* output clause, if specified */ + JsonBehavior *on_empty; /* ON EMPTY behavior, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + JsonWrapper wrapper; /* array wrapper behavior (JSON_QUERY only) */ + bool omit_quotes; /* omit or keep quotes? (JSON_QUERY only) */ + int location; /* token location, or -1 if unknown */ +} JsonFuncExpr; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 85e484bf43..3937114743 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1544,6 +1544,17 @@ typedef struct XmlExpr int location; } XmlExpr; +/* + * JsonExprOp - + * enumeration of JSON functions using JSON path + */ +typedef enum JsonExprOp +{ + JSON_VALUE_OP, /* JSON_VALUE() */ + JSON_QUERY_OP, /* JSON_QUERY() */ + JSON_EXISTS_OP /* JSON_EXISTS() */ +} JsonExprOp; + /* * JsonEncoding - * representation of JSON ENCODING clause @@ -1568,6 +1579,37 @@ typedef enum JsonFormatType * jsonb */ } JsonFormatType; +/* + * JsonBehaviorType - + * enumeration of behavior types used in JSON ON ... BEHAVIOR clause + * + * If enum members are reordered, get_json_behavior() from ruleutils.c + * must be updated accordingly. + */ +typedef enum JsonBehaviorType +{ + JSON_BEHAVIOR_NULL = 0, + JSON_BEHAVIOR_ERROR, + JSON_BEHAVIOR_EMPTY, + JSON_BEHAVIOR_TRUE, + JSON_BEHAVIOR_FALSE, + JSON_BEHAVIOR_UNKNOWN, + JSON_BEHAVIOR_EMPTY_ARRAY, + JSON_BEHAVIOR_EMPTY_OBJECT, + JSON_BEHAVIOR_DEFAULT +} JsonBehaviorType; + +/* + * JsonWrapper - + * representation of WRAPPER clause for JSON_QUERY() + */ +typedef enum JsonWrapper +{ + JSW_NONE, + JSW_CONDITIONAL, + JSW_UNCONDITIONAL, +} JsonWrapper; + /* * JsonFormat - * representation of JSON FORMAT clause @@ -1661,6 +1703,73 @@ typedef struct JsonIsPredicate int location; /* token location, or -1 if unknown */ } JsonIsPredicate; +/* + * JsonBehavior - + * representation of JSON ON ... BEHAVIOR clause + */ +typedef struct JsonBehavior +{ + NodeTag type; + JsonBehaviorType btype; /* behavior type */ + Node *default_expr; /* default expression, if any */ +} JsonBehavior; + +/* + * JsonCoercion - + * coercion from SQL/JSON item types to SQL types + */ +typedef struct JsonCoercion +{ + NodeTag type; + Node *expr; /* resulting expression coerced to target type */ + bool via_populate; /* coerce result using json_populate_type()? */ + bool via_io; /* coerce result using type input function? */ + Oid collation; /* collation for coercion via I/O or populate */ +} JsonCoercion; + +/* + * JsonItemCoercions - + * expressions for coercion from SQL/JSON item types directly to the + * output SQL type + */ +typedef struct JsonItemCoercions +{ + NodeTag type; + JsonCoercion *null; + JsonCoercion *string; + JsonCoercion *numeric; + JsonCoercion *boolean; + JsonCoercion *date; + JsonCoercion *time; + JsonCoercion *timetz; + JsonCoercion *timestamp; + JsonCoercion *timestamptz; + JsonCoercion *composite; /* arrays and objects */ +} JsonItemCoercions; + +/* + * JsonExpr - + * transformed representation of JSON_VALUE(), JSON_QUERY(), JSON_EXISTS() + */ +typedef struct JsonExpr +{ + Expr xpr; + JsonExprOp op; /* json function ID */ + Node *formatted_expr; /* formatted context item expression */ + JsonCoercion *result_coercion; /* resulting coercion to RETURNING type */ + JsonFormat *format; /* context item format (JSON/JSONB) */ + Node *path_spec; /* JSON path specification expression */ + List *passing_names; /* PASSING argument names */ + List *passing_values; /* PASSING argument values */ + JsonReturning *returning; /* RETURNING clause type/format info */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + JsonItemCoercions *coercions; /* coercions for JSON_VALUE */ + JsonWrapper wrapper; /* WRAPPER for JSON_QUERY */ + bool omit_quotes; /* KEEP/OMIT QUOTES for JSON_QUERY */ + int location; /* token location, or -1 if unknown */ +} JsonExpr; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 5984dcfa4b..0954d9fc7b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -93,6 +93,7 @@ PG_KEYWORD("commit", COMMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("committed", COMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("compression", COMPRESSION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("concurrently", CONCURRENTLY, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("conditional", CONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("configuration", CONFIGURATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("conflict", CONFLICT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("connection", CONNECTION, UNRESERVED_KEYWORD, BARE_LABEL) @@ -147,11 +148,13 @@ PG_KEYWORD("double", DOUBLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("drop", DROP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("each", EACH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("empty", EMPTY_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("event", EVENT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("except", EXCEPT, RESERVED_KEYWORD, AS_LABEL) @@ -233,10 +236,14 @@ PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_exists", JSON_EXISTS, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) @@ -302,6 +309,7 @@ PG_KEYWORD("off", OFF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("offset", OFFSET, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("oids", OIDS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("old", OLD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("omit", OMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("on", ON, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("only", ONLY, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("operator", OPERATOR, UNRESERVED_KEYWORD, BARE_LABEL) @@ -344,6 +352,7 @@ PG_KEYWORD("procedures", PROCEDURES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("publication", PUBLICATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("quotes", QUOTES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("range", RANGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("read", READ, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("real", REAL, COL_NAME_KEYWORD, BARE_LABEL) @@ -414,6 +423,7 @@ PG_KEYWORD("stdout", STDOUT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("storage", STORAGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("stored", STORED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("string", STRING_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("subscription", SUBSCRIPTION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("substring", SUBSTRING, COL_NAME_KEYWORD, BARE_LABEL) @@ -449,6 +459,7 @@ PG_KEYWORD("types", TYPES_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uescape", UESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unbounded", UNBOUNDED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uncommitted", UNCOMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("unconditional", UNCONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unencrypted", UNENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("union", UNION, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("unique", UNIQUE, RESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/formatting.h b/src/include/utils/formatting.h index 0cad3a2709..d4ca0f42ff 100644 --- a/src/include/utils/formatting.h +++ b/src/include/utils/formatting.h @@ -17,7 +17,6 @@ #ifndef _FORMATTING_H_ #define _FORMATTING_H_ - extern char *str_tolower(const char *buff, size_t nbytes, Oid collid); extern char *str_toupper(const char *buff, size_t nbytes, Oid collid); extern char *str_initcap(const char *buff, size_t nbytes, Oid collid); @@ -29,5 +28,6 @@ extern char *asc_initcap(const char *buff, size_t nbytes); extern Datum parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, Oid *typid, int32 *typmod, int *tz, struct Node *escontext); +extern bool datetime_format_has_tz(const char *fmt_str); #endif diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index f928e6142a..e628d4fdd0 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -422,6 +422,9 @@ extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, int estimated_len); +extern Jsonb *JsonbMakeEmptyArray(void); +extern Jsonb *JsonbMakeEmptyObject(void); +extern char *JsonbUnquote(Jsonb *jb); extern bool JsonbExtractScalar(JsonbContainer *jbc, JsonbValue *res); extern const char *JsonbTypeName(JsonbValue *val); diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index 586d948c94..fdc63f26d8 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -15,6 +15,7 @@ #define JSONFUNCS_H #include "common/jsonapi.h" +#include "nodes/nodes.h" #include "utils/jsonb.h" /* @@ -86,5 +87,9 @@ extern Datum to_json_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid); extern Datum to_jsonb_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid); +extern Datum json_populate_type(Datum json_val, Oid json_type, + Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext); #endif diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index f0181e045f..4dae78a98c 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -16,7 +16,9 @@ #include "fmgr.h" #include "nodes/pg_list.h" +#include "nodes/primnodes.h" #include "utils/jsonb.h" +#include "utils/jsonfuncs.h" typedef struct { @@ -184,6 +186,7 @@ extern bool jspGetBool(JsonPathItem *v); extern char *jspGetString(JsonPathItem *v, int32 *len); extern bool jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, int i); +extern bool jspIsMutable(JsonPath *path, List *varnames, List *varexprs); extern const char *jspOperationName(JsonPathItemType type); @@ -261,4 +264,28 @@ extern bool jspConvertRegexFlags(uint32 xflags, int *result, struct Node *escontext); +/* + * Evaluation of jsonpath + */ + +/* External variable passed into jsonpath. */ +typedef struct JsonPathVariable +{ + char *name; + Oid typid; + int32 typmod; + Datum value; + bool isnull; +} JsonPathVariable; + +/* SQL/JSON item */ +extern void JsonItemFromDatum(Datum val, Oid typid, int32 typmod, + JsonbValue *res); + +extern bool JsonPathExists(Datum jb, JsonPath *path, List *vars, bool *error); +extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, + bool *empty, bool *error, List *vars); +extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, + bool *error, List *vars); + #endif diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer index 435c139ec2..b2aa44f36d 100644 --- a/src/interfaces/ecpg/preproc/ecpg.trailer +++ b/src/interfaces/ecpg/preproc/ecpg.trailer @@ -651,6 +651,34 @@ var_type: simple_type $$.type_index = mm_strdup("-1"); $$.type_sizeof = NULL; } + | STRING_P + { + if (INFORMIX_MODE) + { + /* In Informix mode, "string" is automatically a typedef */ + $$.type_enum = ECPGt_string; + $$.type_str = mm_strdup("char"); + $$.type_dimension = mm_strdup("-1"); + $$.type_index = mm_strdup("-1"); + $$.type_sizeof = NULL; + } + else + { + /* Otherwise, legal only if user typedef'ed it */ + struct typedefs *this = get_typedef("string", false); + + $$.type_str = (this->type->type_enum == ECPGt_varchar || this->type->type_enum == ECPGt_bytea) ? EMPTY : mm_strdup(this->name); + $$.type_enum = this->type->type_enum; + $$.type_dimension = this->type->type_dimension; + $$.type_index = this->type->type_index; + if (this->type->type_sizeof && strlen(this->type->type_sizeof) != 0) + $$.type_sizeof = this->type->type_sizeof; + else + $$.type_sizeof = cat_str(3, mm_strdup("sizeof("), mm_strdup(this->name), mm_strdup(")")); + + struct_member_list[struct_level] = ECPGstruct_member_dup(this->struct_member_list); + } + } | INTERVAL ecpg_interval { $$.type_enum = ECPGt_interval; diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out new file mode 100644 index 0000000000..8b87580752 --- /dev/null +++ b/src/test/regress/expected/json_sqljson.out @@ -0,0 +1,18 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); +ERROR: JSON_EXISTS() is not yet implemented for the json type +LINE 1: SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_VALUE +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); +ERROR: JSON_VALUE() is not yet implemented for the json type +LINE 1: SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_QUERY +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); +ERROR: JSON_QUERY() is not yet implemented for the json type +LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out new file mode 100644 index 0000000000..22f8679917 --- /dev/null +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -0,0 +1,1042 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL::jsonb, '$'); + json_exists +------------- + +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb 'null', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_EXISTS(jsonb 'null', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{}', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + json_exists +------------- + f +(1 row) + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + json_exists +------------- + t +(1 row) + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); + json_exists +------------- + 1 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); + json_exists +------------- + 0 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); + json_exists +------------- + true +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +ERROR: cannot cast type boolean to jsonb +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +ERROR: cannot cast type boolean to real +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_EXISTS() +LINE 1: ...CT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSO... + ^ +-- JSON_VALUE +SELECT JSON_VALUE(NULL::jsonb, '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$'); + json_value +------------ + true +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + json_value +------------ + t +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); + json_value +------------ + 123 +(1 row) + +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); +ERROR: SQL/JSON item cannot be cast to target type +SELECT JSON_VALUE(jsonb '1.23', '$'); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "1.23" +SELECT JSON_VALUE(jsonb '"aaa"', '$'); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); + json_value +------------ + aa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "aaa" +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); + json_value +------------ + 111 +(1 row) + +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + ?column? +------------ + 03-01-2017 +(1 row) + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '[]', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '{}', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '1', '$.a'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); + json_value +------------ + error +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 3 +(1 row) + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); + json_value +------------ + 0 +(1 row) + +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: " " +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 5 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_VALUE() +LINE 1: ...CT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSO... + ^ +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + x | y +---+---- + 0 | -2 + 1 | 2 + 2 | -1 +(3 rows) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); + json_value +------------ + (1,2) +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + json_value +------------ + (1,2) +(1 row) + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); + json_value +-------------------------- + Tue Feb 20 18:34:56 2018 +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- JSON_QUERY +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + json_query | json_query | json_query | json_query | json_query +--------------------+--------------------+--------------------+----------------------+---------------------- + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, null, "2"] | [1, null, "2"] | [1, null, "2"] | [[1, null, "2"]] | [[1, null, "2"]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] +(6 rows) + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + unspec | without | with cond | with uncond | with +--------------------+--------------------+---------------------+----------------------+---------------------- + | | | | + | | | | + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, 2, 3] | [1, 2, 3] | [1, 2, 3] | [[1, 2, 3]] | [[1, 2, 3]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] + | | [1, "2", null, [3]] | [1, "2", null, [3]] | [1, "2", null, [3]] +(9 rows) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + json_query +------------ + \x616161 +(1 row) + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTE... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTE... + ^ +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_QUERY should return singleton item without wrapper +HINT: Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array. +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); + json_query +------------ + [1, +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); +ERROR: expected JSON array +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + x | y | list +---+---+-------------- + 0 | 0 | [] + 0 | 1 | [1] + 0 | 2 | [1, 2] + 0 | 3 | [1, 2, 3] + 0 | 4 | [1, 2, 3, 4] + 1 | 0 | [] + 1 | 1 | [1] + 1 | 2 | [1, 2] + 1 | 3 | [1, 2, 3] + 1 | 4 | [1, 2, 3, 4] + 2 | 0 | [] + 2 | 1 | [] + 2 | 2 | [2] + 2 | 3 | [2, 3] + 2 | 4 | [2, 3, 4] + 3 | 0 | [] + 3 | 1 | [] + 3 | 2 | [] + 3 | 3 | [3] + 3 | 4 | [3, 4] + 4 | 0 | [] + 4 | 1 | [] + 4 | 2 | [] + 4 | 3 | [] + 4 | 4 | [4] +(25 rows) + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); + json_query +----------------------------------------------------- + (1,aaa,"[1, ""2"", {}]","{""x"": [1, ""2"", {}]}",) +(1 row) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); + unnest +------------------------ + {"a": 1, "b": ["foo"]} + {"a": 2, "c": {}} + 123 +(3 rows) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); + json_query +-------------- + {1,2,NULL,3} +(1 row) + +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); + json_query +------------ + 1 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- Test constraints +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); +\d test_jsonb_constraints + Table "public.test_jsonb_constraints" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+-------------------------------------------------------------------------------- + js | text | | | + i | integer | | | + x | jsonb | | | JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +Check constraints: + "test_jsonb_constraint1" CHECK (js IS JSON) + "test_jsonb_constraint2" CHECK (JSON_EXISTS(js::jsonb, '$."a"' PASSING i + 5 AS int, i::text AS txt, ARRAY[1, 2, 3] AS arr)) + "test_jsonb_constraint3" CHECK (JSON_VALUE(js::jsonb, '$."a"' RETURNING integer DEFAULT ('12'::text || i)::integer ON EMPTY ERROR ON ERROR) > i) + "test_jsonb_constraint4" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb) + "test_jsonb_constraint5" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C")) + "test_jsonb_constraint6" CHECK (JSON_EXISTS(js::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2) + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + check_clause +-------------------------------------------------------------------------------------------------------------------------- + ((JSON_EXISTS((js)::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2)) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C"))) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb)) + ((JSON_VALUE((js)::jsonb, '$."a"' RETURNING integer DEFAULT (('12'::text || i))::integer ON EMPTY ERROR ON ERROR) > i)) + ((js IS JSON)) + (JSON_EXISTS((js)::jsonb, '$."a"' PASSING (i + 5) AS int, (i)::text AS txt, ARRAY[1, 2, 3] AS arr)) +(6 rows) + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +WHERE constraint_name LIKE 'test_jsonb_constraint%'; +ERROR: syntax error at or near "WHERE" +LINE 1: WHERE constraint_name LIKE 'test_jsonb_constraint%'; + ^ +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint1" +DETAIL: Failing row contains (, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains (1, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('[]'); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ([], null, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ({"b": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint3" +DETAIL: Failing row contains ({"a": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint5" +DETAIL: Failing row contains ({"a": 7}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint4" +DETAIL: Failing row contains ({"a": 10}, 1, [1, 2]). +DROP TABLE test_jsonb_constraints; +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 4df9d8503b..9b0ecf049d 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -103,7 +103,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo # ---------- # Another group of parallel tests (JSON related) # ---------- -test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson +test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson json_sqljson jsonb_sqljson # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql new file mode 100644 index 0000000000..4f30fa46b9 --- /dev/null +++ b/src/test/regress/sql/json_sqljson.sql @@ -0,0 +1,11 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + +-- JSON_QUERY + +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql new file mode 100644 index 0000000000..b8a6148b7b --- /dev/null +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -0,0 +1,327 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL::jsonb, '$'); + +SELECT JSON_EXISTS(jsonb '[]', '$'); +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + +SELECT JSON_EXISTS(jsonb '1', '$'); +SELECT JSON_EXISTS(jsonb 'null', '$'); +SELECT JSON_EXISTS(jsonb '[]', '$'); + +SELECT JSON_EXISTS(jsonb '1', '$.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_EXISTS(jsonb 'null', '$.a'); +SELECT JSON_EXISTS(jsonb '[]', '$.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); +SELECT JSON_EXISTS(jsonb '{}', '$.a'); +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSON); -- RETURNING FORMAT not allowed + + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL::jsonb, '$'); + +SELECT JSON_VALUE(jsonb 'null', '$'); +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + +SELECT JSON_VALUE(jsonb 'true', '$'); +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + +SELECT JSON_VALUE(jsonb '123', '$'); +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1.23', '$'); +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '"aaa"', '$'); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); + +SELECT JSON_VALUE(jsonb '[]', '$'); +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '{}', '$'); +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1', '$.a'); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed + +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- JSON_QUERY + +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); + +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); + +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); + +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- Test constraints + +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); + +\d test_jsonb_constraints + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; +WHERE constraint_name LIKE 'test_jsonb_constraint%'; + +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +INSERT INTO test_jsonb_constraints VALUES ('[]'); +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); + +DROP TABLE test_jsonb_constraints; + +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index d251fb9a91..6e4c026492 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1254,14 +1254,24 @@ JsonArrayAgg JsonArrayConstructor JsonArrayQueryConstructor JsonBaseObjectInfo +JsonBehavior +JsonBehaviorType +JsonCoercion +JsonCoercionState +JsonCommon JsonConstructorExpr JsonConstructorExprState JsonConstructorType JsonEncoding +JsonExpr +JsonExprOp +JsonExprState JsonFormat JsonFormatType JsonHashEntry JsonIsPredicate +JsonItemCoercions +JsonItemCoercionsState JsonIterateStringValuesAction JsonKeyValue JsonLexContext @@ -1294,6 +1304,10 @@ JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback JsonPathString +JsonPathVarCallback +JsonPathVariable +JsonPathVariableEvalContext +JsonQuotes JsonReturning JsonSemAction JsonSerializeExpr -- 2.35.3
v6-0004-JSON_TABLE.patch (application/octet-stream, 160.8K) download From 93e11a36f48adce2b70c9d116ad3c737f1fd2684 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:14:12 +0900 Subject: [PATCH v6 4/5] JSON_TABLE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This feature allows jsonb data to be treated as a table and thus used in a FROM clause like other tabular data. Data can be selected from the jsonb using jsonpath expressions, and hoisted out of nested structures in the jsonb to form multiple rows, more or less like an outer join. This also adds the PLAN clauses for JSON_TABLE, which allow the user to specify how data from nested paths are joined, allowing considerable freedom in shaping the tabular output of JSON_TABLE. PLAN DEFAULT allows the user to specify the global strategies when dealing with sibling or child nested paths. The is often sufficient to achieve the necessary goal, and is considerably simpler than the full PLAN clause, which allows the user to specify the strategy to be used for each named nested path. Author: Nikita Glukhov <n.gluhov@postgrespro.ru> Author: Teodor Sigaev <teodor@sigaev.ru> Author: Oleg Bartunov <obartunov@gmail.com> Author: Alexander Korotkov <aekorotkov@gmail.com> Author: Andrew Dunstan <andrew@dunslane.net> Author: Amit Langote <amitlangote09@gmail.com> Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- doc/src/sgml/func.sgml | 448 ++++++++ src/backend/commands/explain.c | 8 +- src/backend/executor/execExprInterp.c | 5 + src/backend/executor/nodeTableFuncscan.c | 27 +- src/backend/nodes/makefuncs.c | 19 + src/backend/nodes/nodeFuncs.c | 30 + src/backend/parser/Makefile | 1 + src/backend/parser/gram.y | 476 ++++++++- src/backend/parser/meson.build | 1 + src/backend/parser/parse_clause.c | 14 +- src/backend/parser/parse_expr.c | 35 +- src/backend/parser/parse_jsontable.c | 739 +++++++++++++ src/backend/parser/parse_relation.c | 5 +- src/backend/parser/parse_target.c | 3 + src/backend/utils/adt/jsonpath_exec.c | 543 ++++++++++ src/backend/utils/adt/ruleutils.c | 279 ++++- src/include/nodes/execnodes.h | 2 + src/include/nodes/makefuncs.h | 2 + src/include/nodes/parsenodes.h | 90 ++ src/include/nodes/primnodes.h | 61 +- src/include/parser/kwlist.h | 4 + src/include/parser/parse_clause.h | 3 + src/include/utils/jsonpath.h | 3 + src/test/regress/expected/json_sqljson.out | 6 + src/test/regress/expected/jsonb_sqljson.out | 1069 +++++++++++++++++++ src/test/regress/sql/json_sqljson.sql | 4 + src/test/regress/sql/jsonb_sqljson.sql | 629 +++++++++++ src/tools/pgindent/typedefs.list | 13 + 28 files changed, 4486 insertions(+), 33 deletions(-) create mode 100644 src/backend/parser/parse_jsontable.c diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index cb36e1463b..44f016369e 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -17144,6 +17144,454 @@ array w/o UK? | t </sect2> + <sect2 id="functions-sqljson-table"> + <title>JSON_TABLE</title> + <indexterm> + <primary>json_table</primary> + </indexterm> + + <para> + <function>json_table</function> is an SQL/JSON function which + queries <acronym>JSON</acronym> data + and presents the results as a relational view, which can be accessed as a + regular SQL table. You can only use <function>json_table</function> inside the + <literal>FROM</literal> clause of a <literal>SELECT</literal> statement. + </para> + + <para> + Taking JSON data as input, <function>json_table</function> uses + a path expression to extract a part of the provided data that + will be used as a <firstterm>row pattern</firstterm> for the + constructed view. Each SQL/JSON item at the top level of the row pattern serves + as the source for a separate row in the constructed relational view. + </para> + + <para> + To split the row pattern into columns, <function>json_table</function> + provides the <literal>COLUMNS</literal> clause that defines the + schema of the created view. For each column to be constructed, + this clause provides a separate path expression that evaluates + the row pattern, extracts a JSON item, and returns it as a + separate SQL value for the specified column. If the required value + is stored in a nested level of the row pattern, it can be extracted + using the <literal>NESTED PATH</literal> subclause. Joining the + columns returned by <literal>NESTED PATH</literal> can add multiple + new rows to the constructed view. Such rows are called + <firstterm>child rows</firstterm>, as opposed to the <firstterm>parent row</firstterm> + that generates them. + </para> + + <para> + The rows produced by <function>JSON_TABLE</function> are laterally + joined to the row that generated them, so you do not have to explicitly join + the constructed view with the original table holding <acronym>JSON</acronym> + data. Optionally, you can specify how to join the columns returned + by <literal>NESTED PATH</literal> using the <literal>PLAN</literal> clause. + </para> + + <para> + Each <literal>NESTED PATH</literal> clause can generate one or more + columns. Columns produced by <literal>NESTED PATH</literal>s at the + same level are considered to be <firstterm>siblings</firstterm>, + while a column produced by a <literal>NESTED PATH</literal> is + considered to be a child of the column produced by a + <literal>NESTED PATH</literal> or row expression at a higher level. + Sibling columns are always joined first. Once they are processed, + the resulting rows are joined to the parent row. + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional></literal> + </term> + <listitem> + <para> + The input data to query, the JSON path expression defining the query, + and an optional <literal>PASSING</literal> clause, which can provide data + values to the <replaceable>path_expression</replaceable>. + The result of the input data + evaluation is called the <firstterm>row pattern</firstterm>. The row + pattern is used as the source for row values in the constructed view. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>COLUMNS</literal>( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + The <literal>COLUMNS</literal> clause defining the schema of the + constructed view. In this clause, you must specify all the columns + to be filled with SQL/JSON items. + The <replaceable>json_table_column</replaceable> + expression has the following syntax variants: + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>name</replaceable> <replaceable>type</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional></literal> + </term> + <listitem> + + <para> + Inserts a single SQL/JSON item into each row of + the specified column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable> + and fills the column with produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>ON EMPTY</literal> and + <literal>ON ERROR</literal> clauses to define how to handle missing values + or structural errors. + <literal>WRAPPER</literal> and <literal>QUOTES</literal> clauses can only + be used with JSON, array, and composite types. + These clauses have the same syntax and semantics as for + <function>json_value</function> and <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> <literal>FORMAT</literal> <replaceable>json_representation</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a composite SQL/JSON + item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable> + and fills the column with produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>WRAPPER</literal>, <literal>QUOTES</literal>, + <literal>ON EMPTY</literal> and <literal>ON ERROR</literal> clauses + to define additional settings for the returned SQL/JSON items. + These clauses have the same syntax and semantics as + for <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> + <literal>EXISTS</literal> <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a boolean item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable>, + checks whether any SQL/JSON items were returned, and fills the column with + resulting boolean value, one for each row. + The specified <replaceable>type</replaceable> should have cast from + <type>boolean</type>. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + </para> + <para> + Optionally, you can add <literal>ON ERROR</literal> clause to define + error behavior. This clause has the same syntax and semantics as + for <function>json_exists</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>NESTED PATH</literal> <replaceable>json_path_specification</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> + <literal>COLUMNS</literal> ( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + Extracts SQL/JSON items from nested levels of the row pattern, + generates one or more columns as defined by the <literal>COLUMNS</literal> + subclause, and inserts the extracted SQL/JSON items into each row of these columns. + The <replaceable>json_table_column</replaceable> expression in the + <literal>COLUMNS</literal> subclause uses the same syntax as in the + parent <literal>COLUMNS</literal> clause. + </para> + + <para> + The <literal>NESTED PATH</literal> syntax is recursive, + so you can go down multiple nested levels by specifying several + <literal>NESTED PATH</literal> subclauses within each other. + It allows to unnest the hierarchy of JSON objects and arrays + in a single function invocation rather than chaining several + <function>JSON_TABLE</function> expressions in an SQL statement. + </para> + + <para> + You can use the <literal>PLAN</literal> clause to define how + to join the columns returned by <literal>NESTED PATH</literal> clauses. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <literal>FOR ORDINALITY</literal> + </term> + <listitem> + + <para> + Adds an ordinality column that provides sequential row numbering. + You can have only one ordinality column per table. Row numbering + is 1-based. For child rows that result from the <literal>NESTED PATH</literal> + clauses, the parent row number is repeated. + </para> + </listitem> + </varlistentry> + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>AS</literal> <replaceable>json_path_name</replaceable> + </term> + <listitem> + + <para> + The optional <replaceable>json_path_name</replaceable> serves as an + identifier of the provided <replaceable>json_path_specification</replaceable>. + The path name must be unique and distinct from the column names. + When using the <literal>PLAN</literal> clause, you must specify the names + for all the paths, including the row pattern. Each path name can appear in + the <literal>PLAN</literal> clause only once. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN</literal> ( <replaceable>json_table_plan</replaceable> ) + </term> + <listitem> + + <para> + Defines how to join the data returned by <literal>NESTED PATH</literal> + clauses to the constructed view. + </para> + <para> + To join columns with parent/child relationship, you can use: + </para> + <variablelist> + <varlistentry> + <term> + <literal>INNER</literal> + </term> + <listitem> + + <para> + Use <literal>INNER JOIN</literal>, so that the parent row + is omitted from the output if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>OUTER</literal> + </term> + <listitem> + + <para> + Use <literal>LEFT OUTER JOIN</literal>, so that the parent row + is always included into the output even if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>, with NULL values + inserted into the child columns if the corresponding + values are missing. + </para> + <para> + This is the default option for joining columns with parent/child relationship. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para> + To join sibling columns, you can use: + </para> + + <variablelist> + <varlistentry> + <term> + <literal>UNION</literal> + </term> + <listitem> + + <para> + Generate one row for each value produced by each of the sibling + columns. The columns from the other siblings are set to null. + </para> + <para> + This is the default option for joining sibling columns. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>CROSS</literal> + </term> + <listitem> + + <para> + Generate one row for each combination of values from the sibling columns. + </para> + </listitem> + </varlistentry> + + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN DEFAULT</literal> ( <literal><replaceable>OUTER | INNER</replaceable> <optional>, <replaceable>UNION | CROSS</replaceable> </optional></literal> ) + </term> + <listitem> + <para> + The terms can also be specified in reverse order. The + <literal>INNER</literal> or <literal>OUTER</literal> option defines the + joining plan for parent/child columns, while <literal>UNION</literal> or + <literal>CROSS</literal> affects joins of sibling columns. This form + of <literal>PLAN</literal> overrides the default plan for + all columns at once. Even though the path names are not included in the + <literal>PLAN DEFAULT</literal> form, to conform to the SQL/JSON standard + they must be provided for all the paths if the <literal>PLAN</literal> + clause is used. + </para> + <para> + <literal>PLAN DEFAULT</literal> is simpler than specifying a complete + <literal>PLAN</literal>, and is often all that is required to get the desired + output. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para>Examples</para> + + <para> + In these examples the following small table storing some JSON data will be used: +<programlisting> +CREATE TABLE my_films ( js jsonb ); + +INSERT INTO my_films VALUES ( +'{ "favorites" : [ + { "kind" : "comedy", "films" : [ + { "title" : "Bananas", + "director" : "Woody Allen"}, + { "title" : "The Dinner Game", + "director" : "Francis Veber" } ] }, + { "kind" : "horror", "films" : [ + { "title" : "Psycho", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "thriller", "films" : [ + { "title" : "Vertigo", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "drama", "films" : [ + { "title" : "Yojimbo", + "director" : "Akira Kurosawa" } ] } + ] }'); +</programlisting> + </para> + <para> + Query the <structname>my_films</structname> table holding + some JSON data about the films and create a view that + distributes the film genre, title, and director between separate columns: +<screen> +SELECT jt.* FROM + my_films, + JSON_TABLE ( js, '$.favorites[*]' COLUMNS ( + id FOR ORDINALITY, + kind text PATH '$.kind', + NESTED PATH '$.films[*]' COLUMNS ( + title text PATH '$.title', + director text PATH '$.director'))) AS jt; +----+----------+------------------+------------------- + id | kind | title | director +----+----------+------------------+------------------- + 1 | comedy | Bananas | Woody Allen + 1 | comedy | The Dinner Game | Francis Veber + 2 | horror | Psycho | Alfred Hitchcock + 3 | thriller | Vertigo | Alfred Hitchcock + 4 | drama | Yojimbo | Akira Kurosawa + (5 rows) +</screen> + </para> + + <para> + Find a director that has done films in two different genres: +<screen> +SELECT + director1 AS director, title1, kind1, title2, kind2 +FROM + my_films, + JSON_TABLE ( js, '$.favorites' AS favs COLUMNS ( + NESTED PATH '$[*]' AS films1 COLUMNS ( + kind1 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film1 COLUMNS ( + title1 text PATH '$.title', + director1 text PATH '$.director') + ), + NESTED PATH '$[*]' AS films2 COLUMNS ( + kind2 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film2 COLUMNS ( + title2 text PATH '$.title', + director2 text PATH '$.director' + ) + ) + ) + PLAN (favs OUTER ((films1 INNER film1) CROSS (films2 INNER film2))) + ) AS jt + WHERE kind1 > kind2 AND director1 = director2; + + director | title1 | kind1 | title2 | kind2 +------------------+---------+----------+--------+-------- + Alfred Hitchcock | Vertigo | thriller | Psycho | horror +(1 row) +</screen> + </para> + </sect2> + <sect2 id="functions-sqljson-path"> <title>The SQL/JSON Path Language</title> diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 8570b14f62..ad899de5d6 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -3862,7 +3862,13 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es) break; case T_TableFuncScan: Assert(rte->rtekind == RTE_TABLEFUNC); - objectname = "xmltable"; + if (rte->tablefunc) + if (rte->tablefunc->functype == TFT_XMLTABLE) + objectname = "xmltable"; + else /* Must be TFT_JSON_TABLE */ + objectname = "json_table"; + else + objectname = NULL; objecttag = "Table Function Name"; break; case T_ValuesScan: diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 4c97e714ea..84c8730129 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -4309,6 +4309,11 @@ ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) break; } + case JSON_TABLE_OP: + res = item; + resnull = false; + break; + default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); *op->resnull = true; diff --git a/src/backend/executor/nodeTableFuncscan.c b/src/backend/executor/nodeTableFuncscan.c index 791cbd2372..085a612533 100644 --- a/src/backend/executor/nodeTableFuncscan.c +++ b/src/backend/executor/nodeTableFuncscan.c @@ -28,6 +28,7 @@ #include "miscadmin.h" #include "nodes/execnodes.h" #include "utils/builtins.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/xml.h" @@ -161,8 +162,9 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) scanstate->ss.ps.qual = ExecInitQual(node->scan.plan.qual, &scanstate->ss.ps); - /* Only XMLTABLE is supported currently */ - scanstate->routine = &XmlTableRoutine; + /* Only XMLTABLE and JSON_TABLE are supported currently */ + scanstate->routine = + tf->functype == TFT_XMLTABLE ? &XmlTableRoutine : &JsonbTableRoutine; scanstate->perTableCxt = AllocSetContextCreate(CurrentMemoryContext, @@ -182,6 +184,10 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) ExecInitExprList(tf->colexprs, (PlanState *) scanstate); scanstate->coldefexprs = ExecInitExprList(tf->coldefexprs, (PlanState *) scanstate); + scanstate->colvalexprs = + ExecInitExprList(tf->colvalexprs, (PlanState *) scanstate); + scanstate->passingvalexprs = + ExecInitExprList(tf->passingvalexprs, (PlanState *) scanstate); scanstate->notnulls = tf->notnulls; @@ -381,14 +387,17 @@ tfuncInitialize(TableFuncScanState *tstate, ExprContext *econtext, Datum doc) routine->SetNamespace(tstate, ns_name, ns_uri); } - /* Install the row filter expression into the table builder context */ - value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); - if (isnull) - ereport(ERROR, - (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), - errmsg("row filter expression must not be null"))); + if (routine->SetRowFilter) + { + /* Install the row filter expression into the table builder context */ + value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); + if (isnull) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("row filter expression must not be null"))); - routine->SetRowFilter(tstate, TextDatumGetCString(value)); + routine->SetRowFilter(tstate, TextDatumGetCString(value)); + } /* * Install the column filter expressions into the table builder context. diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 7bcc12b04f..7a03283713 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -878,6 +878,25 @@ makeJsonBehavior(JsonBehaviorType type, Node *default_expr) return behavior; } +/* + * makeJsonTableJoinedPlan - + * creates a joined JsonTablePlan node + */ +Node * +makeJsonTableJoinedPlan(JsonTablePlanJoinType type, Node *plan1, Node *plan2, + int location) +{ + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_JOINED; + n->join_type = type; + n->plan1 = castNode(JsonTablePlan, plan1); + n->plan2 = castNode(JsonTablePlan, plan2); + n->location = location; + + return (Node *) n; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index d31371bb4d..e07c066e15 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -2631,6 +2631,10 @@ expression_tree_walker_impl(Node *node, return true; if (WALK(tf->coldefexprs)) return true; + if (WALK(tf->colvalexprs)) + return true; + if (WALK(tf->passingvalexprs)) + return true; } break; default: @@ -3699,6 +3703,8 @@ expression_tree_mutator_impl(Node *node, MUTATE(newnode->rowexpr, tf->rowexpr, Node *); MUTATE(newnode->colexprs, tf->colexprs, List *); MUTATE(newnode->coldefexprs, tf->coldefexprs, List *); + MUTATE(newnode->colvalexprs, tf->colvalexprs, List *); + MUTATE(newnode->passingvalexprs, tf->passingvalexprs, List *); return (Node *) newnode; } break; @@ -4130,6 +4136,30 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonTable: + { + JsonTable *jt = (JsonTable *) node; + + if (WALK(jt->common)) + return true; + if (WALK(jt->columns)) + return true; + } + break; + case T_JsonTableColumn: + { + JsonTableColumn *jtc = (JsonTableColumn *) node; + + if (WALK(jtc->typeName)) + return true; + if (WALK(jtc->on_empty)) + return true; + if (WALK(jtc->on_error)) + return true; + if (jtc->coltype == JTC_NESTED && WALK(jtc->columns)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile index 9f1c4022bb..f4c0cc7f10 100644 --- a/src/backend/parser/Makefile +++ b/src/backend/parser/Makefile @@ -23,6 +23,7 @@ OBJS = \ parse_enr.o \ parse_expr.o \ parse_func.o \ + parse_jsontable.o \ parse_merge.o \ parse_node.o \ parse_oper.o \ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index f7ad8f18de..ff1d3332c1 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -654,19 +654,43 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_aggregate_func json_api_common_syntax json_argument + json_table + json_table_column_definition + json_table_ordinality_column_definition + json_table_regular_column_definition + json_table_formatted_column_definition + json_table_exists_column_definition + json_table_nested_columns + json_table_plan_clause_opt + json_table_plan + json_table_plan_simple + json_table_plan_parent_child + json_table_plan_outer + json_table_plan_inner + json_table_plan_sibling + json_table_plan_union + json_table_plan_cross + json_table_plan_primary %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt json_arguments + json_table_columns_clause + json_table_column_definition_list +%type <str> json_table_column_path_specification_clause_opt %type <ival> json_encoding_clause_opt json_predicate_type_constraint json_wrapper_behavior + json_table_default_plan_choices + json_table_default_plan_inner_outer + json_table_default_plan_union_cross %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt %type <jsbehavior> json_value_behavior json_query_behavior json_exists_behavior + json_table_behavior %type <js_quotes> json_quotes_clause_opt /* @@ -732,7 +756,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG - JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_TABLE JSON_VALUE KEY KEYS KEEP @@ -743,8 +767,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE - NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE - NORMALIZE NORMALIZED + NAME_P NAMES NATIONAL NATURAL NCHAR NESTED NEW NEXT NFC NFD NFKC NFKD NO + NONE NORMALIZE NORMALIZED NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC @@ -752,8 +776,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER - PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD - PLACING PLANS POLICY + PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH + PLACING PLAN PLANS POLICY POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION @@ -861,6 +885,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); * Using the same precedence as IDENT seems right for the reasons given above. */ %nonassoc UNBOUNDED /* ideally would have same precedence as IDENT */ +%nonassoc COLUMNS %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' @@ -883,6 +908,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL +%nonassoc json_table_column +%nonassoc NESTED +%left PATH %% /* @@ -13324,6 +13352,21 @@ table_ref: relation_expr opt_alias_clause $2->alias = $4; $$ = (Node *) $2; } + | json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $1); + + jt->alias = $2; + $$ = (Node *) jt; + } + | LATERAL_P json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $2); + + jt->alias = $3; + jt->lateral = true; + $$ = (Node *) jt; + } ; @@ -13891,6 +13934,8 @@ xmltable_column_option_el: { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(true), @1); } | NULL_P { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(false), @1); } + | PATH b_expr + { $$ = makeDefElem("path", $2, @1); } ; xml_namespace_list: @@ -16697,6 +16742,11 @@ json_value_behavior: | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } ; +json_table_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + ; + /* ARRAY is a noise word */ json_wrapper_behavior: WITHOUT WRAPPER { $$ = JSW_NONE; } @@ -16718,6 +16768,414 @@ json_quotes_clause_opt: | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } ; +json_table: + JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + json_table_behavior ON ERROR_P + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_columns_clause: + COLUMNS '(' json_table_column_definition_list ')' { $$ = $3; } + ; + +json_table_column_definition_list: + json_table_column_definition + { $$ = list_make1($1); } + | json_table_column_definition_list ',' json_table_column_definition + { $$ = lappend($1, $3); } + ; + +json_table_column_definition: + json_table_ordinality_column_definition %prec json_table_column + | json_table_regular_column_definition %prec json_table_column + | json_table_formatted_column_definition %prec json_table_column + | json_table_exists_column_definition %prec json_table_column + | json_table_nested_columns + ; + +json_table_ordinality_column_definition: + ColId FOR ORDINALITY + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FOR_ORDINALITY; + n->name = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_regular_column_definition: + ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_exists_column_definition: + ColId Typename + EXISTS json_table_column_path_specification_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + EXISTS json_table_column_path_specification_clause_opt + json_exists_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_column_path_specification_clause_opt: + PATH Sconst { $$ = $2; } + | /* EMPTY */ %prec json_table_column { $$ = NULL; } + ; + +json_table_formatted_column_definition: + ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->on_error = $12; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_nested_columns: + NESTED path_opt Sconst + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = NULL; + n->columns = $4; + n->location = @1; + $$ = (Node *) n; + } + | NESTED path_opt Sconst AS name + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = $5; + n->columns = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +path_opt: + PATH { } + | /* EMPTY */ { } + ; + +json_table_plan_clause_opt: + PLAN '(' json_table_plan ')' { $$ = $3; } + | PLAN DEFAULT '(' json_table_default_plan_choices ')' + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_DEFAULT; + n->join_type = $4; + n->location = @1; + $$ = (Node *) n; + } + | /* EMPTY */ { $$ = NULL; } + ; + +json_table_plan: + json_table_plan_simple + | json_table_plan_parent_child + | json_table_plan_sibling + ; + +json_table_plan_simple: + name + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_SIMPLE; + n->pathname = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_plan_primary: + json_table_plan_simple { $$ = $1; } + | '(' json_table_plan ')' + { + castNode(JsonTablePlan, $2)->location = @1; + $$ = $2; + } + ; + +json_table_plan_outer: + json_table_plan_simple OUTER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_OUTER, $1, $3, @1); } + ; + +json_table_plan_inner: + json_table_plan_simple INNER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_INNER, $1, $3, @1); } + ; + +json_table_plan_parent_child: + json_table_plan_outer + | json_table_plan_inner + ; + +json_table_plan_union: + json_table_plan_primary UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + | json_table_plan_union UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + ; + +json_table_plan_cross: + json_table_plan_primary CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + | json_table_plan_cross CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + ; + +json_table_plan_sibling: + json_table_plan_union + | json_table_plan_cross + ; + +json_table_default_plan_choices: + json_table_default_plan_inner_outer { $$ = $1 | JSTPJ_UNION; } + | json_table_default_plan_union_cross { $$ = $1 | JSTPJ_OUTER; } + | json_table_default_plan_inner_outer ',' + json_table_default_plan_union_cross { $$ = $1 | $3; } + | json_table_default_plan_union_cross ',' + json_table_default_plan_inner_outer { $$ = $1 | $3; } + ; + +json_table_default_plan_inner_outer: + INNER_P { $$ = JSTPJ_INNER; } + | OUTER_P { $$ = JSTPJ_OUTER; } + ; + +json_table_default_plan_union_cross: + UNION { $$ = JSTPJ_UNION; } + | CROSS { $$ = JSTPJ_CROSS; } + ; + json_returning_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17442,6 +17900,7 @@ unreserved_keyword: | MOVE | NAME_P | NAMES + | NESTED | NEW | NEXT | NFC @@ -17476,6 +17935,8 @@ unreserved_keyword: | PARTITION | PASSING | PASSWORD + | PATH + | PLAN | PLANS | POLICY | PRECEDING @@ -17640,6 +18101,7 @@ col_name_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | LEAST | NATIONAL @@ -18008,6 +18470,7 @@ bare_label_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | KEEP | KEY @@ -18047,6 +18510,7 @@ bare_label_keyword: | NATIONAL | NATURAL | NCHAR + | NESTED | NEW | NEXT | NFC @@ -18091,7 +18555,9 @@ bare_label_keyword: | PARTITION | PASSING | PASSWORD + | PATH | PLACING + | PLAN | PLANS | POLICY | POSITION diff --git a/src/backend/parser/meson.build b/src/backend/parser/meson.build index 94517a7f7d..0f0e4ac7ca 100644 --- a/src/backend/parser/meson.build +++ b/src/backend/parser/meson.build @@ -10,6 +10,7 @@ backend_sources += files( 'parse_enr.c', 'parse_expr.c', 'parse_func.c', + 'parse_jsontable.c', 'parse_merge.c', 'parse_node.c', 'parse_oper.c', diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index f61f794755..b44ff44991 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -697,7 +697,11 @@ transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf) char **names; int colno; - /* Currently only XMLTABLE is supported */ + /* + * Currently we only support XMLTABLE here. See transformJsonTable() for + * JSON_TABLE support. + */ + tf->functype = TFT_XMLTABLE; constructName = "XMLTABLE"; docType = XMLOID; @@ -1104,13 +1108,17 @@ transformFromClauseItem(ParseState *pstate, Node *n, rtr->rtindex = nsitem->p_rtindex; return (Node *) rtr; } - else if (IsA(n, RangeTableFunc)) + else if (IsA(n, RangeTableFunc) || IsA(n, JsonTable)) { /* table function is like a plain relation */ RangeTblRef *rtr; ParseNamespaceItem *nsitem; - nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + if (IsA(n, RangeTableFunc)) + nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + else + nsitem = transformJsonTable(pstate, (JsonTable *) n); + *top_nsitem = nsitem; *namespace = list_make1(nsitem); rtr = makeNode(RangeTblRef); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 2ff2b4d01f..89743e91b6 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4252,7 +4252,7 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) JsonFormatType format; char *constructName; - if (func->common->pathname) + if (func->common->pathname && func->op != JSON_TABLE_OP) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("JSON_TABLE path name is not allowed here"), @@ -4271,6 +4271,9 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) case JSON_EXISTS_OP: constructName = "JSON_EXISTS()"; break; + case JSON_TABLE_OP: + constructName = "JSON_TABLE()"; + break; default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); break; @@ -4310,14 +4313,19 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) &jsexpr->passing_values, &jsexpr->passing_names); - if (func->op != JSON_EXISTS_OP) + if (func->op != JSON_EXISTS_OP && func->op != JSON_TABLE_OP) jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, JSON_BEHAVIOR_NULL); - jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, - func->op == JSON_EXISTS_OP ? - JSON_BEHAVIOR_FALSE : - JSON_BEHAVIOR_NULL); + if (func->op == JSON_EXISTS_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_FALSE); + else if (func->op == JSON_TABLE_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_EMPTY); + else + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_NULL); return jsexpr; } @@ -4641,6 +4649,21 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->result_coercion->expr = NULL; } break; + + case JSON_TABLE_OP: + jsexpr->returning = makeNode(JsonReturning); + jsexpr->returning->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + jsexpr->returning->typid = exprType(contextItemExpr); + jsexpr->returning->typmod = -1; + + if (jsexpr->returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("JSON_TABLE() is not yet implemented for the json type"), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + break; } if (exprType(contextItemExpr) != JSONBOID) diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c new file mode 100644 index 0000000000..a7802e0499 --- /dev/null +++ b/src/backend/parser/parse_jsontable.c @@ -0,0 +1,739 @@ +/*------------------------------------------------------------------------- + * + * parse_jsontable.c + * parsing of JSON_TABLE + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/parser/parse_jsontable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/pg_collation.h" +#include "catalog/pg_type.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "optimizer/optimizer.h" +#include "parser/parse_clause.h" +#include "parser/parse_collate.h" +#include "parser/parse_expr.h" +#include "parser/parse_relation.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/json.h" +#include "utils/lsyscache.h" + +/* Context for JSON_TABLE transformation */ +typedef struct JsonTableParseContext +{ + ParseState *pstate; /* parsing state */ + JsonTable *table; /* untransformed node */ + TableFunc *tablefunc; /* transformed node */ + List *pathNames; /* list of all path and columns names */ + int pathNameId; /* path name id counter */ + Oid contextItemTypid; /* type oid of context item (json/jsonb) */ +} JsonTableParseContext; + +static JsonTableParent *transformJsonTableColumns(JsonTableParseContext *cxt, + JsonTablePlan *plan, + List *columns, + char *pathSpec, + char **pathName, + int location); + +static Node * +makeStringConst(char *str, int location) +{ + A_Const *n = makeNode(A_Const); + + n->val.node.type = T_String; + n->val.sval.sval = str; + n->location = location; + + return (Node *) n; +} + +/* + * Transform JSON_TABLE column + * - regular column into JSON_VALUE() + * - FORMAT JSON column into JSON_QUERY() + * - EXISTS column into JSON_EXISTS() + */ +static Node * +transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, + List *passingArgs, bool errorOnError) +{ + JsonFuncExpr *jfexpr = makeNode(JsonFuncExpr); + JsonCommon *common = makeNode(JsonCommon); + JsonOutput *output = makeNode(JsonOutput); + char *pathspec; + JsonFormat *default_format; + + jfexpr->op = + jtc->coltype == JTC_REGULAR ? JSON_VALUE_OP : + jtc->coltype == JTC_EXISTS ? JSON_EXISTS_OP : JSON_QUERY_OP; + jfexpr->common = common; + jfexpr->output = output; + jfexpr->on_empty = jtc->on_empty; + jfexpr->on_error = jtc->on_error; + if (!jfexpr->on_error && errorOnError) + jfexpr->on_error = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); + jfexpr->omit_quotes = jtc->omit_quotes; + jfexpr->wrapper = jtc->wrapper; + jfexpr->location = jtc->location; + + output->typeName = jtc->typeName; + output->returning = makeNode(JsonReturning); + output->returning->format = jtc->format; + + default_format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + + common->pathname = NULL; + common->expr = makeJsonValueExpr((Expr *) contextItemExpr, default_format); + common->passing = passingArgs; + + if (jtc->pathspec) + pathspec = jtc->pathspec; + else + { + /* Construct default path as '$."column_name"' */ + StringInfoData path; + + initStringInfo(&path); + + appendStringInfoString(&path, "$."); + escape_json(&path, jtc->name); + + pathspec = path.data; + } + + common->pathspec = makeStringConst(pathspec, -1); + + return (Node *) jfexpr; +} + +static bool +isJsonTablePathNameDuplicate(JsonTableParseContext *cxt, const char *pathname) +{ + ListCell *lc; + + foreach(lc, cxt->pathNames) + { + if (!strcmp(pathname, (const char *) lfirst(lc))) + return true; + } + + return false; +} + +/* Register the column name in the path name list. */ +static void +registerJsonTableColumn(JsonTableParseContext *cxt, char *colname) +{ + if (isJsonTablePathNameDuplicate(cxt, colname)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_ALIAS), + errmsg("duplicate JSON_TABLE column name: %s", colname), + errhint("JSON_TABLE column names must be distinct from one another."))); + + cxt->pathNames = lappend(cxt->pathNames, colname); +} + +/* Recursively register all nested column names in the path name list. */ +static void +registerAllJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED) + { + if (jtc->pathname) + registerJsonTableColumn(cxt, jtc->pathname); + + registerAllJsonTableColumns(cxt, jtc->columns); + } + else + { + registerJsonTableColumn(cxt, jtc->name); + } + } +} + +/* Generate a new unique JSON_TABLE path name. */ +static char * +generateJsonTablePathName(JsonTableParseContext *cxt) +{ + char namebuf[32]; + char *name = namebuf; + + do + { + snprintf(namebuf, sizeof(namebuf), "json_table_path_%d", + ++cxt->pathNameId); + } while (isJsonTablePathNameDuplicate(cxt, name)); + + name = pstrdup(name); + cxt->pathNames = lappend(cxt->pathNames, name); + + return name; +} + +/* Collect sibling path names from plan to the specified list. */ +static void +collectSiblingPathsInJsonTablePlan(JsonTablePlan *plan, List **paths) +{ + if (plan->plan_type == JSTP_SIMPLE) + *paths = lappend(*paths, plan->pathname); + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + *paths = lappend(*paths, plan->plan1->pathname); + } + else if (plan->join_type == JSTPJ_CROSS || + plan->join_type == JSTPJ_UNION) + { + collectSiblingPathsInJsonTablePlan(plan->plan1, paths); + collectSiblingPathsInJsonTablePlan(plan->plan2, paths); + } + else + elog(ERROR, "invalid JSON_TABLE join type %d", + plan->join_type); + } +} + +/* + * Validate child JSON_TABLE plan by checking that: + * - all nested columns have path names specified + * - all nested columns have corresponding node in the sibling plan + * - plan does not contain duplicate or extra nodes + */ +static void +validateJsonTableChildPlan(ParseState *pstate, JsonTablePlan *plan, + List *columns) +{ + ListCell *lc1; + List *siblings = NIL; + int nchildren = 0; + + if (plan) + collectSiblingPathsInJsonTablePlan(plan, &siblings); + + foreach(lc1, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc1)); + + if (jtc->coltype == JTC_NESTED) + { + ListCell *lc2; + bool found = false; + + if (!jtc->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("nested JSON_TABLE columns must contain an explicit AS pathname specification if an explicit PLAN clause is used"), + parser_errposition(pstate, jtc->location))); + + /* find nested path name in the list of sibling path names */ + foreach(lc2, siblings) + { + if ((found = !strcmp(jtc->pathname, lfirst(lc2)))) + break; + } + + if (!found) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node for nested path %s was not found in plan.", jtc->pathname), + parser_errposition(pstate, jtc->location))); + + nchildren++; + } + } + + if (list_length(siblings) > nchildren) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node contains some extra or duplicate sibling nodes."), + parser_errposition(pstate, plan ? plan->location : -1))); +} + +static JsonTableColumn * +findNestedJsonTableColumn(List *columns, const char *pathname) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED && + jtc->pathname && + !strcmp(jtc->pathname, pathname)) + return jtc; + } + + return NULL; +} + +static Node * +transformNestedJsonTableColumn(JsonTableParseContext *cxt, JsonTableColumn *jtc, + JsonTablePlan *plan) +{ + JsonTableParent *node; + char *pathname = jtc->pathname; + + node = transformJsonTableColumns(cxt, plan, jtc->columns, jtc->pathspec, + &pathname, jtc->location); + + return (Node *) node; +} + +static Node * +makeJsonTableSiblingJoin(bool cross, Node *lnode, Node *rnode) +{ + JsonTableSibling *join = makeNode(JsonTableSibling); + + join->larg = lnode; + join->rarg = rnode; + join->cross = cross; + + return (Node *) join; +} + +/* + * Recursively transform child JSON_TABLE plan. + * + * Default plan is transformed into a cross/union join of its nested columns. + * Simple and outer/inner plans are transformed into a JsonTableParent by + * finding and transforming corresponding nested column. + * Sibling plans are recursively transformed into a JsonTableSibling. + */ +static Node * +transformJsonTableChildPlan(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns) +{ + JsonTableColumn *jtc = NULL; + + if (!plan || plan->plan_type == JSTP_DEFAULT) + { + /* unspecified or default plan */ + Node *res = NULL; + ListCell *lc; + bool cross = plan && (plan->join_type & JSTPJ_CROSS); + + /* transform all nested columns into cross/union join */ + foreach(lc, columns) + { + JsonTableColumn *col = castNode(JsonTableColumn, lfirst(lc)); + Node *node; + + if (col->coltype != JTC_NESTED) + continue; + + node = transformNestedJsonTableColumn(cxt, col, plan); + + /* join transformed node with previous sibling nodes */ + res = res ? makeJsonTableSiblingJoin(cross, res, node) : node; + } + + return res; + } + else if (plan->plan_type == JSTP_SIMPLE) + { + jtc = findNestedJsonTableColumn(columns, plan->pathname); + } + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + jtc = findNestedJsonTableColumn(columns, plan->plan1->pathname); + } + else + { + Node *node1 = transformJsonTableChildPlan(cxt, plan->plan1, + columns); + Node *node2 = transformJsonTableChildPlan(cxt, plan->plan2, + columns); + + return makeJsonTableSiblingJoin(plan->join_type == JSTPJ_CROSS, + node1, node2); + } + } + else + elog(ERROR, "invalid JSON_TABLE plan type %d", plan->plan_type); + + if (!jtc) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name was %s not found in nested columns list.", + plan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + return transformNestedJsonTableColumn(cxt, jtc, plan); +} + +/* Check whether type is json/jsonb, array, or record. */ +static bool +typeIsComposite(Oid typid) +{ + char typtype; + + if (typid == JSONOID || + typid == JSONBOID || + typid == RECORDOID || + type_is_array(typid)) + return true; + + typtype = get_typtype(typid); + + if (typtype == TYPTYPE_COMPOSITE) + return true; + + if (typtype == TYPTYPE_DOMAIN) + return typeIsComposite(getBaseType(typid)); + + return false; +} + +/* Append transformed non-nested JSON_TABLE columns to the TableFunc node */ +static void +appendJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *col; + ParseState *pstate = cxt->pstate; + JsonTable *jt = cxt->table; + TableFunc *tf = cxt->tablefunc; + bool errorOnError = jt->on_error && + jt->on_error->btype == JSON_BEHAVIOR_ERROR; + + foreach(col, columns) + { + JsonTableColumn *rawc = castNode(JsonTableColumn, lfirst(col)); + Oid typid; + int32 typmod; + Node *colexpr; + + if (rawc->name) + { + /* make sure column names are unique */ + ListCell *colname; + + foreach(colname, tf->colnames) + if (!strcmp((const char *) colname, rawc->name)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("column name \"%s\" is not unique", + rawc->name), + parser_errposition(pstate, rawc->location))); + + tf->colnames = lappend(tf->colnames, + makeString(pstrdup(rawc->name))); + } + + /* + * Determine the type and typmod for the new column. FOR ORDINALITY + * columns are INTEGER by standard; the others are user-specified. + */ + switch (rawc->coltype) + { + case JTC_FOR_ORDINALITY: + colexpr = NULL; + typid = INT4OID; + typmod = -1; + break; + + case JTC_REGULAR: + typenameTypeIdAndMod(pstate, rawc->typeName, &typid, &typmod); + + /* + * Use implicit FORMAT JSON for composite types (arrays and + * records) + */ + if (typeIsComposite(typid)) + rawc->coltype = JTC_FORMATTED; + else if (rawc->wrapper != JSW_NONE) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use WITH WRAPPER clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + else if (rawc->omit_quotes) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use OMIT QUOTES clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + + /* FALLTHROUGH */ + case JTC_EXISTS: + case JTC_FORMATTED: + { + Node *je; + CaseTestExpr *param = makeNode(CaseTestExpr); + + param->collation = InvalidOid; + param->typeId = cxt->contextItemTypid; + param->typeMod = -1; + + je = transformJsonTableColumn(rawc, (Node *) param, + NIL, errorOnError); + + colexpr = transformExpr(pstate, je, EXPR_KIND_FROM_FUNCTION); + assign_expr_collations(pstate, colexpr); + + typid = exprType(colexpr); + typmod = exprTypmod(colexpr); + break; + } + + case JTC_NESTED: + continue; + + default: + elog(ERROR, "unknown JSON_TABLE column type: %d", rawc->coltype); + break; + } + + tf->coltypes = lappend_oid(tf->coltypes, typid); + tf->coltypmods = lappend_int(tf->coltypmods, typmod); + tf->colcollations = lappend_oid(tf->colcollations, get_typcollation(typid)); + tf->colvalexprs = lappend(tf->colvalexprs, colexpr); + } +} + +/* + * Create transformed JSON_TABLE parent plan node by appending all non-nested + * columns to the TableFunc node and remembering their indices in the + * colvalexprs list. + */ +static JsonTableParent * +makeParentJsonTableNode(JsonTableParseContext *cxt, char *pathSpec, char *pathName, + List *columns) +{ + JsonTableParent *node = makeNode(JsonTableParent); + + node->path = makeNode(JsonTablePath); + node->path->value = makeConst(JSONPATHOID, -1, InvalidOid, -1, + DirectFunctionCall1(jsonpath_in, + CStringGetDatum(pathSpec)), + false, false); + if (pathName) + node->path->name = pstrdup(pathName); + + /* save start of column range */ + node->colMin = list_length(cxt->tablefunc->colvalexprs); + + appendJsonTableColumns(cxt, columns); + + /* save end of column range */ + node->colMax = list_length(cxt->tablefunc->colvalexprs) - 1; + + node->errorOnError = + cxt->table->on_error && + cxt->table->on_error->btype == JSON_BEHAVIOR_ERROR; + + return node; +} + +static JsonTableParent * +transformJsonTableColumns(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns, char *pathSpec, char **pathName, + int location) +{ + JsonTableParent *node; + JsonTablePlan *childPlan; + bool defaultPlan = !plan || plan->plan_type == JSTP_DEFAULT; + + if (!*pathName) + { + if (cxt->table->plan) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE expression"), + errdetail("JSON_TABLE columns must contain " + "explicit AS pathname specification if " + "explicit PLAN clause is used"), + parser_errposition(cxt->pstate, location))); + + *pathName = generateJsonTablePathName(cxt); + } + + if (defaultPlan) + childPlan = plan; + else + { + /* validate parent and child plans */ + JsonTablePlan *parentPlan; + + if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type != JSTPJ_INNER && + plan->join_type != JSTPJ_OUTER) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Expected INNER or OUTER JSON_TABLE plan node."), + parser_errposition(cxt->pstate, plan->location))); + + parentPlan = plan->plan1; + childPlan = plan->plan2; + + Assert(parentPlan->plan_type != JSTP_JOINED); + Assert(parentPlan->pathname); + } + else + { + parentPlan = plan; + childPlan = NULL; + } + + if (strcmp(parentPlan->pathname, *pathName)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name mismatch: expected %s but %s is given.", + *pathName, parentPlan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + validateJsonTableChildPlan(cxt->pstate, childPlan, columns); + } + + /* transform only non-nested columns */ + node = makeParentJsonTableNode(cxt, pathSpec, *pathName, columns); + + if (childPlan || defaultPlan) + { + /* transform recursively nested columns */ + node->child = transformJsonTableChildPlan(cxt, childPlan, columns); + if (node->child) + node->outerJoin = !plan || (plan->join_type & JSTPJ_OUTER); + /* else: default plan case, no children found */ + } + + return node; +} + +/* + * transformJsonTable - + * Transform a raw JsonTable into TableFunc. + * + * Transform the document-generating expression, the row-generating expression, + * the column-generating expressions, and the default value expressions. + */ +ParseNamespaceItem * +transformJsonTable(ParseState *pstate, JsonTable *jt) +{ + JsonTableParseContext cxt; + TableFunc *tf = makeNode(TableFunc); + JsonFuncExpr *jfe = makeNode(JsonFuncExpr); + JsonExpr *je; + JsonTablePlan *plan = jt->plan; + JsonCommon *jscommon; + char *rootPathName = jt->common->pathname; + char *rootPath; + bool is_lateral; + + cxt.pstate = pstate; + cxt.table = jt; + cxt.tablefunc = tf; + cxt.pathNames = NIL; + cxt.pathNameId = 0; + + if (rootPathName) + registerJsonTableColumn(&cxt, rootPathName); + + registerAllJsonTableColumns(&cxt, jt->columns); + +#if 0 /* XXX it' unclear from the standard whether + * root path name is mandatory or not */ + if (plan && plan->plan_type != JSTP_DEFAULT && !rootPathName) + { + /* Assign root path name and create corresponding plan node */ + JsonTablePlan *rootNode = makeNode(JsonTablePlan); + JsonTablePlan *rootPlan = (JsonTablePlan *) + makeJsonTableJoinedPlan(JSTPJ_OUTER, (Node *) rootNode, + (Node *) plan, jt->location); + + rootPathName = generateJsonTablePathName(&cxt); + + rootNode->plan_type = JSTP_SIMPLE; + rootNode->pathname = rootPathName; + + plan = rootPlan; + } +#endif + + jscommon = copyObject(jt->common); + jscommon->pathspec = makeStringConst(pstrdup("$"), -1); + + jfe->op = JSON_TABLE_OP; + jfe->common = jscommon; + jfe->on_error = jt->on_error; + jfe->location = jt->common->location; + + /* + * We make lateral_only names of this level visible, whether or not the + * RangeTableFunc is explicitly marked LATERAL. This is needed for SQL + * spec compliance and seems useful on convenience grounds for all + * functions in FROM. + * + * (LATERAL can't nest within a single pstate level, so we don't need + * save/restore logic here.) + */ + Assert(!pstate->p_lateral_active); + pstate->p_lateral_active = true; + + tf->functype = TFT_JSON_TABLE; + tf->docexpr = transformExpr(pstate, (Node *) jfe, EXPR_KIND_FROM_FUNCTION); + + cxt.contextItemTypid = exprType(tf->docexpr); + + if (!IsA(jt->common->pathspec, A_Const) || + castNode(A_Const, jt->common->pathspec)->val.node.type != T_String) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("only string constants supported in JSON_TABLE path specification"), + parser_errposition(pstate, + exprLocation(jt->common->pathspec)))); + + rootPath = castNode(A_Const, jt->common->pathspec)->val.sval.sval; + + tf->plan = (Node *) transformJsonTableColumns(&cxt, plan, jt->columns, + rootPath, &rootPathName, + jt->common->location); + + /* Also save a copy of the PASSING arguments in the TableFunc node. */ + je = (JsonExpr *) tf->docexpr; + tf->passingvalexprs = copyObject(je->passing_values); + + tf->ordinalitycol = -1; /* undefine ordinality column number */ + tf->location = jt->location; + + pstate->p_lateral_active = false; + + /* + * Mark the RTE as LATERAL if the user said LATERAL explicitly, or if + * there are any lateral cross-references in it. + */ + is_lateral = jt->lateral || contain_vars_of_level((Node *) tf, 0); + + return addRangeTableEntryForTableFunc(pstate, + tf, jt->alias, is_lateral, true); +} diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 41d60494b9..7da42c4772 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -2072,7 +2072,8 @@ addRangeTableEntryForTableFunc(ParseState *pstate, Assert(list_length(tf->coltypmods) == list_length(tf->colnames)); Assert(list_length(tf->colcollations) == list_length(tf->colnames)); - refname = alias ? alias->aliasname : pstrdup("xmltable"); + refname = alias ? alias->aliasname : + pstrdup(tf->functype == TFT_XMLTABLE ? "xmltable" : "json_table"); rte->rtekind = RTE_TABLEFUNC; rte->relid = InvalidOid; @@ -2095,7 +2096,7 @@ addRangeTableEntryForTableFunc(ParseState *pstate, ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), errmsg("%s function has %d columns available but %d columns specified", - "XMLTABLE", + tf->functype == TFT_XMLTABLE ? "XMLTABLE" : "JSON_TABLE", list_length(tf->colnames), numaliases))); rte->eref = eref; diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4f94fc69d6..6500e42485 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1992,6 +1992,9 @@ FigureColnameInternal(Node *node, char **name) case JSON_EXISTS_OP: *name = "json_exists"; return 2; + case JSON_TABLE_OP: + *name = "json_table"; + return 2; } break; default: diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index eded22e194..92d76d5cde 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -61,10 +61,12 @@ #include "catalog/pg_collation.h" #include "catalog/pg_type.h" +#include "executor/execExpr.h" #include "funcapi.h" #include "lib/stringinfo.h" #include "miscadmin.h" #include "nodes/miscnodes.h" +#include "nodes/nodeFuncs.h" #include "regex/regex.h" #include "utils/builtins.h" #include "utils/date.h" @@ -75,6 +77,8 @@ #include "utils/guc.h" #include "utils/json.h" #include "utils/jsonpath.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" #include "utils/timestamp.h" #include "utils/varlena.h" @@ -156,6 +160,61 @@ typedef struct JsonValueListIterator ListCell *next; } JsonValueListIterator; +/* Structures for JSON_TABLE execution */ + +typedef enum JsonTablePlanStateType +{ + JSON_TABLE_SCAN_STATE = 0, + JSON_TABLE_JOIN_STATE +} JsonTablePlanStateType; + +typedef struct JsonTablePlanState +{ + JsonTablePlanStateType type; + + struct JsonTablePlanState *parent; + struct JsonTablePlanState *nested; +} JsonTablePlanState; + +typedef struct JsonTableScanState +{ + JsonTablePlanState plan; + + MemoryContext mcxt; + JsonPath *path; + List *args; + JsonValueList found; + JsonValueListIterator iter; + Datum current; + int ordinal; + bool currentIsNull; + bool outerJoin; + bool errorOnError; + bool advanceNested; + bool reset; +} JsonTableScanState; + +typedef struct JsonTableJoinState +{ + JsonTablePlanState plan; + + JsonTablePlanState *left; + JsonTablePlanState *right; + bool cross; + bool advanceRight; +} JsonTableJoinState; + +/* random number to identify JsonTableExecContext */ +#define JSON_TABLE_EXEC_CONTEXT_MAGIC 418352867 + +typedef struct JsonTableExecContext +{ + int magic; + JsonTableScanState **colexprscans; + JsonTableScanState *root; + bool empty; +} JsonTableExecContext; + /* strict/lax flags is decomposed into four [un]wrap/error flags */ #define jspStrictAbsenseOfErrors(cxt) (!(cxt)->laxMode) #define jspAutoUnwrap(cxt) ((cxt)->laxMode) @@ -248,6 +307,7 @@ static JsonPathExecResult getArrayIndex(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbValue *jb, int32 *index); static JsonBaseObjectInfo setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id); +static void JsonValueListClear(JsonValueList *jvl); static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv); static int JsonValueListLength(const JsonValueList *jvl); static bool JsonValueListIsEmpty(JsonValueList *jvl); @@ -265,6 +325,14 @@ static JsonbValue *wrapItemsInArray(const JsonValueList *items); static int compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, bool useTz, bool *cast_error); + +static JsonTablePlanState *JsonTableInitPlanState(JsonTableExecContext *cxt, + Node *plan, + JsonTablePlanState *parent); +static bool JsonTablePlanNextRow(JsonTablePlanState *state); +static bool JsonTableScanNextRow(JsonTableScanState *scan); + + /****************** User interface to JsonPath executor ********************/ /* @@ -2518,6 +2586,13 @@ setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id) return baseObject; } +static void +JsonValueListClear(JsonValueList *jvl) +{ + jvl->singleton = NULL; + jvl->list = NULL; +} + static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv) { @@ -3123,3 +3198,471 @@ JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); } } + +/************************ JSON_TABLE functions ***************************/ + +/* + * Returns private data from executor state. Ensure validity by check with + * MAGIC number. + */ +static inline JsonTableExecContext * +GetJsonTableExecContext(TableFuncScanState *state, const char *fname) +{ + JsonTableExecContext *result; + + if (!IsA(state, TableFuncScanState)) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + result = (JsonTableExecContext *) state->opaque; + if (result->magic != JSON_TABLE_EXEC_CONTEXT_MAGIC) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + + return result; +} + +/* Recursively initialize JSON_TABLE scan / join state */ +static JsonTableJoinState * +JsonTableInitJoinState(JsonTableExecContext *cxt, JsonTableSibling *plan, + JsonTablePlanState *parent) +{ + JsonTableJoinState *join = palloc0(sizeof(*join)); + + join->plan.type = JSON_TABLE_JOIN_STATE; + /* parent and nested not set. */ + + join->cross = plan->cross; + join->left = JsonTableInitPlanState(cxt, plan->larg, parent); + join->right = JsonTableInitPlanState(cxt, plan->rarg, parent); + + return join; +} + +static JsonTableScanState * +JsonTableInitScanState(JsonTableExecContext *cxt, JsonTableParent *plan, + JsonTablePlanState *parent, + List *args, MemoryContext mcxt) +{ + JsonTableScanState *scan = palloc0(sizeof(*scan)); + int i; + + scan->plan.type = JSON_TABLE_SCAN_STATE; + scan->plan.parent = parent; + + scan->outerJoin = plan->outerJoin; + scan->errorOnError = plan->errorOnError; + scan->path = DatumGetJsonPathP(plan->path->value->constvalue); + scan->args = args; + scan->mcxt = AllocSetContextCreate(mcxt, "JsonTableExecContext", + ALLOCSET_DEFAULT_SIZES); + + /* + * Set after settings scan->args and scan->mcxt, because the recursive call + * wants to use those values. + */ + scan->plan.nested = plan->child ? + JsonTableInitPlanState(cxt, plan->child, (JsonTablePlanState *) scan) : + NULL; + + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + + for (i = plan->colMin; i <= plan->colMax; i++) + cxt->colexprscans[i] = scan; + + return scan; +} + +/* Recursively initialize JSON_TABLE scan state */ +static JsonTablePlanState * +JsonTableInitPlanState(JsonTableExecContext *cxt, Node *plan, + JsonTablePlanState *parent) +{ + JsonTablePlanState *state; + + if (IsA(plan, JsonTableSibling)) + { + JsonTableSibling *join = castNode(JsonTableSibling, plan); + + state = (JsonTablePlanState *) + JsonTableInitJoinState(cxt, join, parent); + } + else + { + JsonTableParent *scan = castNode(JsonTableParent, plan); + JsonTableScanState *parent_scan = (JsonTableScanState *) parent; + + Assert(parent_scan); + state = (JsonTablePlanState *) + JsonTableInitScanState(cxt, scan, parent, parent_scan->args, + parent_scan->mcxt); + } + + return state; +} + +/* + * JsonTableInitOpaque + * Fill in TableFuncScanState->opaque for JsonTable processor + */ +static void +JsonTableInitOpaque(TableFuncScanState *state, int natts) +{ + JsonTableExecContext *cxt; + PlanState *ps = &state->ss.ps; + TableFuncScan *tfs = castNode(TableFuncScan, ps->plan); + TableFunc *tf = tfs->tablefunc; + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + JsonExpr *je = castNode(JsonExpr, tf->docexpr); + List *args = NIL; + + cxt = palloc0(sizeof(JsonTableExecContext)); + cxt->magic = JSON_TABLE_EXEC_CONTEXT_MAGIC; + + if (state->passingvalexprs) + { + ListCell *exprlc; + ListCell *namelc; + + Assert(list_length(state->passingvalexprs) == + list_length(je->passing_names)); + forboth(exprlc, state->passingvalexprs, + namelc, je->passing_names) + { + ExprState *state = lfirst_node(ExprState, exprlc); + String *name = lfirst_node(String, namelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(name->sval); + var->typid = exprType((Node *) state->expr); + var->typmod = exprTypmod((Node *) state->expr); + + /* + * Evaluate the expression and save the value to be returned by + * GetJsonPathVar(). + */ + var->value = ExecEvalExpr(state, ps->ps_ExprContext, + &var->isnull); + + args = lappend(args, var); + } + } + + cxt->colexprscans = palloc(sizeof(JsonTableScanState *) * + list_length(tf->colvalexprs)); + + cxt->root = JsonTableInitScanState(cxt, root, NULL, args, + CurrentMemoryContext); + state->opaque = cxt; +} + +/* Reset scan iterator to the beginning of the item list */ +static void +JsonTableRescan(JsonTableScanState *scan) +{ + JsonValueListInitIterator(&scan->found, &scan->iter); + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + scan->advanceNested = false; + scan->ordinal = 0; +} + +/* Reset context item of a scan, execute JSON path and reset a scan */ +static void +JsonTableResetContextItem(JsonTableScanState *scan, Datum item) +{ + MemoryContext oldcxt; + JsonPathExecResult res; + Jsonb *js = (Jsonb *) DatumGetJsonbP(item); + + JsonValueListClear(&scan->found); + + MemoryContextResetOnly(scan->mcxt); + + oldcxt = MemoryContextSwitchTo(scan->mcxt); + + res = executeJsonPath(scan->path, scan->args, GetJsonPathVar, js, + scan->errorOnError, &scan->found, + false /* FIXME */ ); + + MemoryContextSwitchTo(oldcxt); + + if (jperIsError(res)) + { + Assert(!scan->errorOnError); + JsonValueListClear(&scan->found); /* EMPTY ON ERROR case */ + } + + JsonTableRescan(scan); +} + +/* + * JsonTableSetDocument + * Install the input document + */ +static void +JsonTableSetDocument(TableFuncScanState *state, Datum value) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableSetDocument"); + + JsonTableResetContextItem(cxt->root, value); +} + +/* Recursively reset scan and its child nodes */ +static void +JsonTableRescanRecursive(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTableRescanRecursive(join->left); + JsonTableRescanRecursive(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan = (JsonTableScanState *) state; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + JsonTableRescan(scan); + if (scan->plan.nested) + JsonTableRescanRecursive(scan->plan.nested); + } +} + +/* + * Fetch next row from a cross/union joined scan. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTablePlanNextRow(JsonTablePlanState *state) +{ + JsonTableJoinState *join; + + if (state->type == JSON_TABLE_SCAN_STATE) + return JsonTableScanNextRow((JsonTableScanState *) state); + + join = (JsonTableJoinState *) state; + if (join->advanceRight) + { + /* fetch next inner row */ + if (JsonTablePlanNextRow(join->right)) + return true; + + /* inner rows are exhausted */ + if (join->cross) + join->advanceRight = false; /* next outer row */ + else + return false; /* end of scan */ + } + + while (!join->advanceRight) + { + /* fetch next outer row */ + bool left = JsonTablePlanNextRow(join->left); + + if (join->cross) + { + if (!left) + return false; /* end of scan */ + + JsonTableRescanRecursive(join->right); + + if (!JsonTablePlanNextRow(join->right)) + continue; /* next outer row */ + + join->advanceRight = true; /* next inner row */ + } + else if (!left) + { + if (!JsonTablePlanNextRow(join->right)) + return false; /* end of scan */ + + join->advanceRight = true; /* next inner row */ + } + + break; + } + + return true; +} + +/* Recursively set 'reset' flag of scan and its child nodes */ +static void +JsonTablePlanReset(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTablePlanReset(join->left); + JsonTablePlanReset(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + scan = (JsonTableScanState *) state; + scan->reset = true; + scan->advanceNested = false; + + if (scan->plan.nested) + JsonTablePlanReset(scan->plan.nested); + } +} + +/* + * Fetch next row from a simple scan with outer/inner joined nested subscans. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTableScanNextRow(JsonTableScanState *scan) +{ + /* reset context item if requested */ + if (scan->reset) + { + JsonTableScanState *parent_scan = + (JsonTableScanState *) scan->plan.parent; + + Assert(parent_scan && !parent_scan->currentIsNull); + JsonTableResetContextItem(scan, parent_scan->current); + scan->reset = false; + } + + if (scan->advanceNested) + { + /* fetch next nested row */ + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested) + return true; + } + + for (;;) + { + /* fetch next row */ + JsonbValue *jbv = JsonValueListNext(&scan->found, &scan->iter); + MemoryContext oldcxt; + + if (!jbv) + { + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + return false; /* end of scan */ + } + + /* set current row item */ + oldcxt = MemoryContextSwitchTo(scan->mcxt); + scan->current = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + scan->currentIsNull = false; + MemoryContextSwitchTo(oldcxt); + + scan->ordinal++; + + if (!scan->plan.nested) + break; + + JsonTablePlanReset(scan->plan.nested); + + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested || scan->outerJoin) + break; + } + + return true; +} + +/* + * JsonTableFetchRow + * Prepare the next "current" tuple for upcoming GetValue calls. + * Returns FALSE if the row-filter expression returned no more rows. + */ +static bool +JsonTableFetchRow(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableFetchRow"); + + if (cxt->empty) + return false; + + return JsonTableScanNextRow(cxt->root); +} + +/* + * JsonTableGetValue + * Return the value for column number 'colnum' for the current row. + * + * This leaks memory, so be sure to reset often the context in which it's + * called. + */ +static Datum +JsonTableGetValue(TableFuncScanState *state, int colnum, + Oid typid, int32 typmod, bool *isnull) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableGetValue"); + ExprContext *econtext = state->ss.ps.ps_ExprContext; + ExprState *estate = list_nth(state->colvalexprs, colnum); + JsonTableScanState *scan = cxt->colexprscans[colnum]; + Datum result; + + if (scan->currentIsNull) /* NULL from outer/union join */ + { + result = (Datum) 0; + *isnull = true; + } + else if (estate) /* regular column */ + { + Datum saved_caseValue = econtext->caseValue_datum; + bool saved_caseIsNull = econtext->caseValue_isNull; + + /* Pass the value for CaseTestExpr that may be present in colexpr */ + econtext->caseValue_datum = scan->current; + econtext->caseValue_isNull = false; + + result = ExecEvalExpr(estate, econtext, isnull); + + econtext->caseValue_datum = saved_caseValue; + econtext->caseValue_isNull = saved_caseIsNull; + } + else + { + result = Int32GetDatum(scan->ordinal); /* ordinality column */ + *isnull = false; + } + + return result; +} + +/* + * JsonTableDestroyOpaque + */ +static void +JsonTableDestroyOpaque(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableDestroyOpaque"); + + /* not valid anymore */ + cxt->magic = 0; + + state->opaque = NULL; +} + +const TableFuncRoutine JsonbTableRoutine = +{ + JsonTableInitOpaque, + JsonTableSetDocument, + NULL, + NULL, + NULL, + JsonTableFetchRow, + JsonTableGetValue, + JsonTableDestroyOpaque +}; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d1ec418ccf..e89d1e03d6 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -522,6 +522,8 @@ static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); static void get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit); +static void get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8606,7 +8608,8 @@ get_json_behavior(JsonBehavior *behavior, deparse_context *context, /* * get_json_expr_options * - * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS and + * JSON_TABLE columns. */ static void get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, @@ -9854,6 +9857,9 @@ get_rule_expr(Node *node, deparse_context *context, case JSON_EXISTS_OP: appendStringInfoString(buf, "JSON_EXISTS("); break; + default: + elog(ERROR, "unexpected JsonExpr type: %d", jexpr->op); + break; } get_rule_expr(jexpr->formatted_expr, context, showimplicit); @@ -11207,16 +11213,14 @@ get_sublink_expr(SubLink *sublink, deparse_context *context) /* ---------- - * get_tablefunc - Parse back a table function + * get_xmltable - Parse back a XMLTABLE function * ---------- */ static void -get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +get_xmltable(TableFunc *tf, deparse_context *context, bool showimplicit) { StringInfo buf = context->buf; - /* XMLTABLE is the only existing implementation. */ - appendStringInfoString(buf, "XMLTABLE("); if (tf->ns_uris != NIL) @@ -11307,6 +11311,271 @@ get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) appendStringInfoChar(buf, ')'); } +/* + * get_json_nested_columns - Parse back nested JSON_TABLE columns + */ +static void +get_json_table_nested_columns(TableFunc *tf, Node *node, + deparse_context *context, bool showimplicit, + bool needcomma) +{ + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_nested_columns(tf, n->larg, context, showimplicit, + needcomma); + get_json_table_nested_columns(tf, n->rarg, context, showimplicit, true); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + if (needcomma) + appendStringInfoChar(context->buf, ','); + + appendStringInfoChar(context->buf, ' '); + appendContextKeyword(context, "NESTED PATH ", 0, 0, 0); + get_const_expr(n->path->value, context, -1); + appendStringInfo(context->buf, " AS %s", quote_identifier(n->path->name)); + get_json_table_columns(tf, n, context, showimplicit); + } +} + +/* + * get_json_table_plan - Parse back a JSON_TABLE plan + */ +static void +get_json_table_plan(TableFunc *tf, Node *node, deparse_context *context, + bool parenthesize) +{ + if (parenthesize) + appendStringInfoChar(context->buf, '('); + + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_plan(tf, n->larg, context, + IsA(n->larg, JsonTableSibling) || + castNode(JsonTableParent, n->larg)->child); + + appendStringInfoString(context->buf, n->cross ? " CROSS " : " UNION "); + + get_json_table_plan(tf, n->rarg, context, + IsA(n->rarg, JsonTableSibling) || + castNode(JsonTableParent, n->rarg)->child); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + appendStringInfoString(context->buf, quote_identifier(n->path->name)); + + if (n->child) + { + appendStringInfoString(context->buf, + n->outerJoin ? " OUTER " : " INNER "); + get_json_table_plan(tf, n->child, context, + IsA(n->child, JsonTableSibling)); + } + } + + if (parenthesize) + appendStringInfoChar(context->buf, ')'); +} + +/* + * get_json_table_columns - Parse back JSON_TABLE columns + */ +static void +get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + ListCell *lc_colname; + ListCell *lc_coltype; + ListCell *lc_coltypmod; + ListCell *lc_colvalexpr; + int colnum = 0; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "COLUMNS (", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forfour(lc_colname, tf->colnames, + lc_coltype, tf->coltypes, + lc_coltypmod, tf->coltypmods, + lc_colvalexpr, tf->colvalexprs) + { + char *colname = strVal(lfirst(lc_colname)); + JsonExpr *colexpr; + Oid typid; + int32 typmod; + bool ordinality; + JsonBehaviorType default_behavior; + + typid = lfirst_oid(lc_coltype); + typmod = lfirst_int(lc_coltypmod); + colexpr = castNode(JsonExpr, lfirst(lc_colvalexpr)); + + if (colnum < node->colMin) + { + colnum++; + continue; + } + + if (colnum > node->colMax) + break; + + if (colnum > node->colMin) + appendStringInfoString(buf, ", "); + + colnum++; + + ordinality = !colexpr; + + appendContextKeyword(context, "", 0, 0, 0); + + appendStringInfo(buf, "%s %s", quote_identifier(colname), + ordinality ? "FOR ORDINALITY" : + format_type_with_typemod(typid, typmod)); + if (ordinality) + continue; + + if (colexpr->op == JSON_EXISTS_OP) + { + appendStringInfoString(buf, " EXISTS"); + default_behavior = JSON_BEHAVIOR_FALSE; + } + else + { + if (colexpr->op == JSON_QUERY_OP) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(typid, &typcategory, &typispreferred); + + if (typcategory == TYPCATEGORY_STRING) + appendStringInfoString(buf, + colexpr->format->format_type == JS_FORMAT_JSONB ? + " FORMAT JSONB" : " FORMAT JSON"); + } + + default_behavior = JSON_BEHAVIOR_NULL; + } + + if (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR) + default_behavior = JSON_BEHAVIOR_ERROR; + + appendStringInfoString(buf, " PATH "); + + get_json_path_spec(colexpr->path_spec, context, showimplicit); + + get_json_expr_options(colexpr, context, default_behavior); + } + + if (node->child) + get_json_table_nested_columns(tf, node->child, context, showimplicit, + node->colMax >= node->colMin); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_json_table - Parse back a JSON_TABLE function + * ---------- + */ +static void +get_json_table(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + + appendStringInfoString(buf, "JSON_TABLE("); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_const_expr(root->path->value, context, -1); + + appendStringInfo(buf, " AS %s", quote_identifier(root->path->name)); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PASSING ", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr((Node *) lfirst(lc2), context, false); + appendStringInfo(buf, " AS %s", + quote_identifier((lfirst_node(String, lc1))->sval) + ); + } + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + } + + get_json_table_columns(tf, root, context, showimplicit); + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PLAN ", 0, 0, 0); + get_json_table_plan(tf, (Node *) root, context, true); + + if (jexpr->on_error->btype != JSON_BEHAVIOR_EMPTY) + get_json_behavior(jexpr->on_error, context, "ERROR"); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_tablefunc - Parse back a table function + * ---------- + */ +static void +get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + /* XMLTABLE and JSON_TABLE are the only existing implementations. */ + + if (tf->functype == TFT_XMLTABLE) + get_xmltable(tf, context, showimplicit); + else if (tf->functype == TFT_JSON_TABLE) + get_json_table(tf, context, showimplicit); +} + /* ---------- * get_from_clause - Parse back a FROM clause * diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 584bf7001a..91453681e3 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1879,6 +1879,8 @@ typedef struct TableFuncScanState ExprState *rowexpr; /* state for row-generating expression */ List *colexprs; /* state for column-generating expression */ List *coldefexprs; /* state for column default expressions */ + List *colvalexprs; /* state for column value expression */ + List *passingvalexprs; /* state for PASSING argument expression */ List *ns_names; /* same as TableFunc.ns_names */ List *ns_uris; /* list of states of namespace URI exprs */ Bitmapset *notnulls; /* nullability flag for each output column */ diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 5e149b0266..d8466a2699 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -112,6 +112,8 @@ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *expr, JsonFormat *format); extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); +extern Node *makeJsonTableJoinedPlan(JsonTablePlanJoinType type, + Node *plan1, Node *plan2, int location); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 7d63a37d5f..b15f71cc92 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1733,6 +1733,19 @@ typedef enum JsonQuotes */ typedef char *JsonPathSpec; +/* + * JsonTableColumnType - + * enumeration of JSON_TABLE column types + */ +typedef enum JsonTableColumnType +{ + JTC_FOR_ORDINALITY, + JTC_REGULAR, + JTC_EXISTS, + JTC_FORMATTED, + JTC_NESTED, +} JsonTableColumnType; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1786,6 +1799,83 @@ typedef struct JsonFuncExpr int location; /* token location, or -1 if unknown */ } JsonFuncExpr; +/* + * JsonTableColumn - + * untransformed representation of JSON_TABLE column + */ +typedef struct JsonTableColumn +{ + NodeTag type; + JsonTableColumnType coltype; /* column type */ + char *name; /* column name */ + TypeName *typeName; /* column type name */ + char *pathspec; /* path specification, if any */ + char *pathname; /* path name, if any */ + JsonFormat *format; /* JSON format clause, if specified */ + JsonWrapper wrapper; /* WRAPPER behavior for formatted columns */ + bool omit_quotes; /* omit or keep quotes on scalar strings? */ + List *columns; /* nested columns */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + int location; /* token location, or -1 if unknown */ +} JsonTableColumn; + +/* + * JsonTablePlanType - + * flags for JSON_TABLE plan node types representation + */ +typedef enum JsonTablePlanType +{ + JSTP_DEFAULT, + JSTP_SIMPLE, + JSTP_JOINED, +} JsonTablePlanType; + +/* + * JsonTablePlanJoinType - + * flags for JSON_TABLE join types representation + */ +typedef enum JsonTablePlanJoinType +{ + JSTPJ_INNER = 0x01, + JSTPJ_OUTER = 0x02, + JSTPJ_CROSS = 0x04, + JSTPJ_UNION = 0x08, +} JsonTablePlanJoinType; + +typedef struct JsonTablePlan JsonTablePlan; + +/* + * JsonTablePlan - + * untransformed representation of JSON_TABLE plan node + */ +struct JsonTablePlan +{ + NodeTag type; + JsonTablePlanType plan_type; /* plan type */ + JsonTablePlanJoinType join_type; /* join type (for joined plan only) */ + JsonTablePlan *plan1; /* first joined plan */ + JsonTablePlan *plan2; /* second joined plan */ + char *pathname; /* path name (for simple plan only) */ + int location; /* token location, or -1 if unknown */ +}; + +/* + * JsonTable - + * untransformed representation of JSON_TABLE + */ +typedef struct JsonTable +{ + NodeTag type; + JsonCommon *common; /* common JSON path syntax fields */ + List *columns; /* list of JsonTableColumn */ + JsonTablePlan *plan; /* join plan, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + Alias *alias; /* table alias in FROM clause */ + bool lateral; /* does it have LATERAL prefix? */ + int location; /* token location, or -1 if unknown */ +} JsonTable; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 3937114743..cea15cc4e5 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -86,8 +86,14 @@ typedef struct RangeVar int location; } RangeVar; +typedef enum TableFuncType +{ + TFT_XMLTABLE, + TFT_JSON_TABLE +} TableFuncType; + /* - * TableFunc - node for a table function, such as XMLTABLE. + * TableFunc - node for a table function, such as XMLTABLE or JSON_TABLE. * * Entries in the ns_names list are either String nodes containing * literal namespace names, or NULL pointers to represent DEFAULT. @@ -95,6 +101,8 @@ typedef struct RangeVar typedef struct TableFunc { NodeTag type; + /* XMLTABLE or JSON_TABLE */ + TableFuncType functype; /* list of namespace URI expressions */ List *ns_uris pg_node_attr(query_jumble_ignore); /* list of namespace names or NULL */ @@ -115,8 +123,14 @@ typedef struct TableFunc List *colexprs; /* list of column default expressions */ List *coldefexprs pg_node_attr(query_jumble_ignore); + /* list of column value expressions */ + List *colvalexprs pg_node_attr(query_jumble_ignore); + /* list of PASSING argument expressions */ + List *passingvalexprs pg_node_attr(query_jumble_ignore); /* nullability flag for each output column */ Bitmapset *notnulls pg_node_attr(query_jumble_ignore); + /* JSON_TABLE plan */ + Node *plan pg_node_attr(query_jumble_ignore); /* counts from 0; -1 if none specified */ int ordinalitycol pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ @@ -1552,7 +1566,8 @@ typedef enum JsonExprOp { JSON_VALUE_OP, /* JSON_VALUE() */ JSON_QUERY_OP, /* JSON_QUERY() */ - JSON_EXISTS_OP /* JSON_EXISTS() */ + JSON_EXISTS_OP, /* JSON_EXISTS() */ + JSON_TABLE_OP /* JSON_TABLE() */ } JsonExprOp; /* @@ -1770,6 +1785,48 @@ typedef struct JsonExpr int location; /* token location, or -1 if unknown */ } JsonExpr; +/* + * JsonTablePath + * A JSON path expression to be computed as part of evaluating + * a JSON_TABLE plan node + */ +typedef struct JsonTablePath +{ + NodeTag type; + + Const *value; + char *name; +} JsonTablePath; + +/* + * JsonTableParent - + * transformed representation of parent JSON_TABLE plan node + */ +typedef struct JsonTableParent +{ + NodeTag type; + JsonTablePath *path; + Node *child; /* nested columns, if any */ + bool outerJoin; /* outer or inner join for nested columns? */ + int colMin; /* min column index in the resulting column + * list */ + int colMax; /* max column index in the resulting column + * list */ + bool errorOnError; /* ERROR/EMPTY ON ERROR behavior */ +} JsonTableParent; + +/* + * JsonTableSibling - + * transformed representation of joined sibling JSON_TABLE plan node + */ +typedef struct JsonTableSibling +{ + NodeTag type; + Node *larg; /* left join node */ + Node *rarg; /* right join node */ + bool cross; /* cross or union join? */ +} JsonTableSibling; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0954d9fc7b..f88a6c9ac6 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -242,6 +242,7 @@ PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_table", JSON_TABLE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) @@ -284,6 +285,7 @@ PG_KEYWORD("names", NAMES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("national", NATIONAL, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("natural", NATURAL, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("nchar", NCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("nested", NESTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("new", NEW, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("next", NEXT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("nfc", NFC, UNRESERVED_KEYWORD, BARE_LABEL) @@ -334,7 +336,9 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h index 8e26482db4..7152a8ef48 100644 --- a/src/include/parser/parse_clause.h +++ b/src/include/parser/parse_clause.h @@ -51,4 +51,7 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle, extern Index assignSortGroupRef(TargetEntry *tle, List *tlist); extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList); +/* functions in parse_jsontable.c */ +extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt); + #endif /* PARSE_CLAUSE_H */ diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index 4dae78a98c..b7ada77cb1 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -15,6 +15,7 @@ #define JSONPATH_H #include "fmgr.h" +#include "executor/tablefunc.h" #include "nodes/pg_list.h" #include "nodes/primnodes.h" #include "utils/jsonb.h" @@ -288,4 +289,6 @@ extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars); +extern PGDLLIMPORT const TableFuncRoutine JsonbTableRoutine; + #endif diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out index 8b87580752..995f267404 100644 --- a/src/test/regress/expected/json_sqljson.out +++ b/src/test/regress/expected/json_sqljson.out @@ -16,3 +16,9 @@ ERROR: JSON_QUERY() is not yet implemented for the json type LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); ^ HINT: Try casting the argument to jsonb +-- JSON_TABLE +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); +ERROR: JSON_TABLE() is not yet implemented for the json type +LINE 1: SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo ... + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out index 22f8679917..4323ed6b35 100644 --- a/src/test/regress/expected/jsonb_sqljson.out +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -1040,3 +1040,1072 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.dateti ERROR: functions in index expression must be marked IMMUTABLE CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; +-- JSON_TABLE +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); +ERROR: syntax error at or near "(" +LINE 1: SELECT JSON_TABLE('[]', '$'); + ^ +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); +ERROR: syntax error at or near ")" +LINE 1: SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + ^ +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); +ERROR: JSON_TABLE function has 1 columns available but 2 columns specified +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + foo +----- +(0 rows) + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + item | foo +------+----- + 123 | +(1 row) + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + js | id | id2 | int | text | char(4) | bool | numeric | domain | js | jb | jst | jsc | jsv | jsb | jsbq | aaa | aaa1 | exists1 | exists2 | exists3 | exists4 | js2 | jsb2w | jsb2q | ia | ta | jba +---------------------------------------------------------------------------------------+----+-----+-----+---------+---------+------+---------+---------+--------------+--------------+--------------+------+------+--------------+--------------+-----+------+---------+---------+---------+---------+--------------+----------------+--------------+----+----+----- + 1 | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [] | | | | | | | | | | | | | | | | | | | | | | | | | | | + {} | 1 | 1 | | | | | | | {} | {} | {} | {} | {} | {} | {} | | | f | 0 | | false | {} | [{}] | {} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 2 | 2 | 1 | 1.23 | 1.23 | | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | | | f | 0 | | false | 1.23 | [1.23] | 1.23 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 3 | 3 | 2 | 2 | 2 | | 2 | 2 | "2" | "2" | "2" | "2" | "2" | "2" | 2 | | | f | 0 | | false | "2" | ["2"] | 2 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 4 | 4 | | aaaaaaa | aaaa | | | aaaaaaa | "aaaaaaa" | "aaaaaaa" | "aaaaaaa" | "aaa | "aaa | "aaaaaaa" | | | | f | 0 | | false | "aaaaaaa" | ["aaaaaaa"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 5 | 5 | | foo | foo | | | | "foo" | "foo" | "foo" | "foo | "foo | "foo" | | | | f | 0 | | false | "foo" | ["foo"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 6 | 6 | | | | | | | null | null | null | null | null | null | null | | | f | 0 | | false | null | [null] | null | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 7 | 7 | 0 | false | fals | f | | false | false | false | false | fals | fals | false | false | | | f | 0 | | false | false | [false] | false | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 8 | 8 | 1 | true | true | t | | true | true | true | true | true | true | true | true | | | f | 0 | | false | true | [true] | true | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 9 | 9 | | | | | | | {"aaa": 123} | {"aaa": 123} | {"aaa": 123} | {"aa | {"aa | {"aaa": 123} | {"aaa": 123} | 123 | 123 | t | 1 | 1 | true | {"aaa": 123} | [{"aaa": 123}] | {"aaa": 123} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 10 | 10 | | [1,2] | [1,2 | | | [1,2] | "[1,2]" | "[1,2]" | "[1,2]" | "[1, | "[1, | "[1,2]" | [1, 2] | | | f | 0 | | false | "[1,2]" | ["[1,2]"] | [1, 2] | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 11 | 11 | | "str" | "str | | | "str" | "\"str\"" | "\"str\"" | "\"str\"" | "\"s | "\"s | "\"str\"" | "str" | | | f | 0 | | false | "\"str\"" | ["\"str\""] | "str" | | | +(14 rows) + +-- JSON_TABLE: Test backward parsing +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); +\sv jsonb_table_view +CREATE OR REPLACE VIEW public.jsonb_table_view AS + SELECT id, + id2, + "int", + text, + "char(4)", + bool, + "numeric", + domain, + js, + jb, + jst, + jsc, + jsv, + jsb, + jsbq, + aaa, + aaa1, + exists1, + exists2, + exists3, + js2, + jsb2w, + jsb2q, + ia, + ta, + jba, + a1, + b1, + a11, + a21, + a22 + FROM JSON_TABLE( + 'null'::jsonb, '$[*]' AS json_table_path_1 + PASSING + 1 + 2 AS a, + '"foo"'::json AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, + "int" integer PATH '$', + text text PATH '$', + "char(4)" character(4) PATH '$', + bool boolean PATH '$', + "numeric" numeric PATH '$', + domain jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc character(4) FORMAT JSON PATH '$', + jsv character varying(4) FORMAT JSON PATH '$', + jsb jsonb PATH '$', + jsbq jsonb PATH '$' OMIT QUOTES, + aaa integer PATH '$."aaa"', + aaa1 integer PATH '$."aaa"', + exists1 boolean EXISTS PATH '$."aaa"', + exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia integer[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 + COLUMNS ( + a1 integer PATH '$."a1"', + b1 text PATH '$."b1"', + NESTED PATH '$[*]' AS "p1 1" + COLUMNS ( + a11 text PATH '$."a11"' + ) + ), + NESTED PATH '$[2]' AS p2 + COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" + COLUMNS ( + a21 text PATH '$."a21"' + ), + NESTED PATH '$[*]' AS p22 + COLUMNS ( + a22 text PATH '$."a22"' + ) + ) + ) + PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22)))) + ) +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Table Function Scan on "json_table" + Output: "json_table".id, "json_table".id2, "json_table"."int", "json_table".text, "json_table"."char(4)", "json_table".bool, "json_table"."numeric", "json_table".domain, "json_table".js, "json_table".jb, "json_table".jst, "json_table".jsc, "json_table".jsv, "json_table".jsb, "json_table".jsbq, "json_table".aaa, "json_table".aaa1, "json_table".exists1, "json_table".exists2, "json_table".exists3, "json_table".js2, "json_table".jsb2w, "json_table".jsb2q, "json_table".ia, "json_table".ta, "json_table".jba, "json_table".a1, "json_table".b1, "json_table".a11, "json_table".a21, "json_table".a22 + Table Function Call: JSON_TABLE('null'::jsonb, '$[*]' AS json_table_path_1 PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (id FOR ORDINALITY, id2 FOR ORDINALITY, "int" integer PATH '$', text text PATH '$', "char(4)" character(4) PATH '$', bool boolean PATH '$', "numeric" numeric PATH '$', domain jsonb_test_domain PATH '$', js json PATH '$', jb jsonb PATH '$', jst text FORMAT JSON PATH '$', jsc character(4) FORMAT JSON PATH '$', jsv character varying(4) FORMAT JSON PATH '$', jsb jsonb PATH '$', jsbq jsonb PATH '$' OMIT QUOTES, aaa integer PATH '$."aaa"', aaa1 integer PATH '$."aaa"', exists1 boolean EXISTS PATH '$."aaa"', exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, js2 json PATH '$', jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, jsb2q jsonb PATH '$' OMIT QUOTES, ia integer[] PATH '$', ta text[] PATH '$', jba jsonb[] PATH '$', NESTED PATH '$[1]' AS p1 COLUMNS (a1 integer PATH '$."a1"', b1 text PATH '$."b1"', NESTED PATH '$[*]' AS "p1 1" COLUMNS (a11 text PATH '$."a11"')), NESTED PATH '$[2]' AS p2 COLUMNS ( NESTED PATH '$[*]' AS "p2:1" COLUMNS (a21 text PATH '$."a21"'), NESTED PATH '$[*]' AS p22 COLUMNS (a22 text PATH '$."a22"'))) PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22))))) +(3 rows) + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + js | a +-------+--- + 1 | 1 + "err" | +(2 rows) + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; + a +--- + +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: jsonpath member accessor can only be applied to an object +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: no SQL/JSON item +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 1 +(1 row) + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); + a +--- + 0 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to smallint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to bigint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to real +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 E... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); + a +----- + fal +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to json +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to jsonb +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EX... + ^ +-- JSON_TABLE: nested paths and plans +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb '[]', '$' -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 4: NESTED PATH '$' COLUMNS ( -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: b +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +-- JSON_TABLE: plan validation +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p1) + ^ +DETAIL: Path name mismatch: expected p0 but p1 is given. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 UNION p1 UNION p11) + ^ +DETAIL: Expected INNER or OUTER JSON_TABLE plan node. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 8: NESTED PATH '$' AS p2 COLUMNS ( + ^ +DETAIL: Plan node for nested path p2 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 5: NESTED PATH '$' AS p11 COLUMNS ( foo int ), + ^ +DETAIL: Plan node for nested path p11 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) + ^ +DETAIL: Plan node contains some extra or duplicate sibling nodes. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 6: NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ^ +DETAIL: Plan node for nested path p12 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 9: NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ^ +DETAIL: Plan node for nested path p21 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + bar | foo | baz +-----+-----+----- +(0 rows) + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb 'null', 'strict $[*]' -- without root path name + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- JSON_TABLE: plan execution +CREATE TEMP TABLE jsonb_table_test (js jsonb); +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + n | a | c | b +---+----+----+--- + 1 | 1 | | + 2 | 2 | 10 | + 2 | 2 | | + 2 | 2 | 20 | + 2 | 2 | | 1 + 2 | 2 | | 2 + 2 | 2 | | 3 + 3 | 3 | | 1 + 3 | 3 | | 2 + 4 | -1 | | 1 + 4 | -1 | | 2 +(11 rows) + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + n | a | b | b1 | c | c1 | b +---+---+--------------+-----+------+----+----- + 1 | 1 | [1, 10] | 1 | 1 | | 101 + 1 | 1 | [1, 10] | 1 | null | | 101 + 1 | 1 | [1, 10] | 1 | 2 | | 101 + 1 | 1 | [1, 10] | 10 | 1 | | 110 + 1 | 1 | [1, 10] | 10 | null | | 110 + 1 | 1 | [1, 10] | 10 | 2 | | 110 + 1 | 1 | [2] | 2 | 1 | | 102 + 1 | 1 | [2] | 2 | null | | 102 + 1 | 1 | [2] | 2 | 2 | | 102 + 1 | 1 | [3, 30, 300] | 3 | 1 | | 103 + 1 | 1 | [3, 30, 300] | 3 | null | | 103 + 1 | 1 | [3, 30, 300] | 3 | 2 | | 103 + 1 | 1 | [3, 30, 300] | 30 | 1 | | 130 + 1 | 1 | [3, 30, 300] | 30 | null | | 130 + 1 | 1 | [3, 30, 300] | 30 | 2 | | 130 + 1 | 1 | [3, 30, 300] | 300 | 1 | | 400 + 1 | 1 | [3, 30, 300] | 300 | null | | 400 + 1 | 1 | [3, 30, 300] | 300 | 2 | | 400 + 2 | 2 | | | | | + 3 | | | | | | +(20 rows) + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + x | y | y | z +---+---+--------------+--- + 2 | 1 | [1, 2, 3] | 1 + 2 | 1 | [1, 2, 3] | 2 + 2 | 1 | [1, 2, 3] | 3 + 3 | 1 | [1, 2, 3] | 1 + 3 | 1 | [1, 2, 3] | 2 + 3 | 1 | [1, 2, 3] | 3 + 3 | 1 | [2, 3, 4, 5] | 2 + 3 | 1 | [2, 3, 4, 5] | 3 + 3 | 1 | [2, 3, 4, 5] | 4 + 3 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [1, 2, 3] | 1 + 4 | 1 | [1, 2, 3] | 2 + 4 | 1 | [1, 2, 3] | 3 + 4 | 1 | [2, 3, 4, 5] | 2 + 4 | 1 | [2, 3, 4, 5] | 3 + 4 | 1 | [2, 3, 4, 5] | 4 + 4 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [3, 4, 5, 6] | 3 + 4 | 1 | [3, 4, 5, 6] | 4 + 4 | 1 | [3, 4, 5, 6] | 5 + 4 | 1 | [3, 4, 5, 6] | 6 + 2 | 2 | [1, 2, 3] | 2 + 2 | 2 | [1, 2, 3] | 3 + 3 | 2 | [1, 2, 3] | 2 + 3 | 2 | [1, 2, 3] | 3 + 3 | 2 | [2, 3, 4, 5] | 2 + 3 | 2 | [2, 3, 4, 5] | 3 + 3 | 2 | [2, 3, 4, 5] | 4 + 3 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [1, 2, 3] | 2 + 4 | 2 | [1, 2, 3] | 3 + 4 | 2 | [2, 3, 4, 5] | 2 + 4 | 2 | [2, 3, 4, 5] | 3 + 4 | 2 | [2, 3, 4, 5] | 4 + 4 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [3, 4, 5, 6] | 3 + 4 | 2 | [3, 4, 5, 6] | 4 + 4 | 2 | [3, 4, 5, 6] | 5 + 4 | 2 | [3, 4, 5, 6] | 6 + 2 | 3 | [1, 2, 3] | 3 + 3 | 3 | [1, 2, 3] | 3 + 3 | 3 | [2, 3, 4, 5] | 3 + 3 | 3 | [2, 3, 4, 5] | 4 + 3 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [1, 2, 3] | 3 + 4 | 3 | [2, 3, 4, 5] | 3 + 4 | 3 | [2, 3, 4, 5] | 4 + 4 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [3, 4, 5, 6] | 3 + 4 | 3 | [3, 4, 5, 6] | 4 + 4 | 3 | [3, 4, 5, 6] | 5 + 4 | 3 | [3, 4, 5, 6] | 6 +(52 rows) + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; +ERROR: could not find jsonpath variable "x" +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); + json_value +------------ + foo +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_query +------------ + 123 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); + json_query +------------ + [123] +(1 row) + +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +ERROR: syntax error at or near " " of jsonpath input +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); +ERROR: only string constants supported in JSON_TABLE path specification +LINE 1: SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || '... + ^ diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql index 4f30fa46b9..df4a430d88 100644 --- a/src/test/regress/sql/json_sqljson.sql +++ b/src/test/regress/sql/json_sqljson.sql @@ -9,3 +9,7 @@ SELECT JSON_VALUE(NULL FORMAT JSON, '$'); -- JSON_QUERY SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + +-- JSON_TABLE + +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql index b8a6148b7b..fbd86f2084 100644 --- a/src/test/regress/sql/jsonb_sqljson.sql +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -325,3 +325,632 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; + +-- JSON_TABLE + +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); + +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); + +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); + +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + +-- JSON_TABLE: Test backward parsing + +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); + +\sv jsonb_table_view + +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; + +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); + +-- JSON_TABLE: nested paths and plans + +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; + +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; + +-- JSON_TABLE: plan validation + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; + +-- JSON_TABLE: plan execution + +CREATE TEMP TABLE jsonb_table_test (js jsonb); + +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); + +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; + +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 6e4c026492..642a29d8d5 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1303,6 +1303,7 @@ JsonPathKeyword JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback +JsonPathSpec JsonPathString JsonPathVarCallback JsonPathVariable @@ -1311,6 +1312,17 @@ JsonQuotes JsonReturning JsonSemAction JsonSerializeExpr +JsonTable +JsonTableColumn +JsonTableColumnType +JsonTableContext +JsonTableJoinState +JsonTableParent +JsonTablePlan +JsonTablePlanJoinType +JsonTablePlanType +JsonTableScanState +JsonTableSibling JsonTokenType JsonTransformStringValuesAction JsonTypeCategory @@ -2765,6 +2777,7 @@ TableFunc TableFuncRoutine TableFuncScan TableFuncScanState +TableFuncType TableInfo TableLikeClause TableSampleClause -- 2.35.3
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-13 16:54  Alvaro Herrera <alvherre@alvh.no-ip.org>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Alvaro Herrera @ 2023-07-13 16:54 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: pgsql-hackers; Andrew Dunstan <andrew@dunslane.net>

I looked at your 0001.  My 0001 are some trivial comment cleanups to
that.

I scrolled through all of jsonfuncs.c to see if there was a better place
for the new function than the end of the file.  Man, is that one ugly
file.  There are almost no comments!  I almost wish you would create a
new file so that you don't have to put this new function in such bad
company.  But maybe it'll improve someday, so ... whatever.

In the original code, the functions here being (re)moved do not need to
return a type output function in a few cases.  This works okay when the
functions are each contained in a single file (because each function
knows that the respective datum_to_json/datum_to_jsonb user of the
returned values won't need the function OID in those other cases); but
as an exported function, that strange API doesn't seem great.  (It only
works for 0002 because the only thing that the executor does with these
cached values is call datum_to_json/b).  That seems easy to solve, since
we can return the hardcoded output function OID in those cases anyway.
A possible complaint about this is that the OID so returned would be
untested code, so they might be wrong and we'd never know.  However,
ISTM it's better to make a promise about always returning a function OID
and later fixing any bogus function OID if we ever discover that we
return one, rather than having to document in the function's comment
that "we only return function OIDs in such and such cases".  So I made
this change my 0002.

A similar complain can be made about which casts we look for.  Right
now, only an explicit cast to JSON is useful, so that's the only thing
we do.  But maybe one day a cast to JSONB would become useful if there's
no cast to JSON for some datatype (in the is_jsonb case only?); and
maybe another type of cast would be useful.  However, that seems like
going too much into uncharted territory with no useful use case, so
let's just not go there for now.  Maybe in the future we can improve
this aspect of it, if need arises.

-- 
Álvaro Herrera        Breisgau, Deutschland  —  https://www.EnterpriseDB.com/

Attachments:

0001-trivial-fixups.notpatch (text/plain, 2.4K) download From 44504a8fe96ad76a375c77e5f53e4f897e3ca2f2 Mon Sep 17 00:00:00 2001 From: Alvaro Herrera <alvherre@alvh.no-ip.org> Date: Thu, 13 Jul 2023 18:06:07 +0200 Subject: [PATCH 1/2] trivial fixups --- src/backend/utils/adt/jsonfuncs.c | 5 ++--- src/include/utils/jsonfuncs.h | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index 612bbf06a3..764d48505b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -5691,9 +5691,8 @@ json_get_first_token(text *json, bool throw_error) * Determine how we want to print values of a given type in datum_to_json(b). * * Given the datatype OID, return its JsonTypeCategory, as well as the type's - * output function OID. If the returned category is JSONTYPE_CAST or - * JSOBTYPE_CASTJSON, we return the OID of the type->JSON cast function - * instead. + * output function OID. If the returned category is JSONTYPE_CAST, we return + * the OID of the type->JSON cast function instead. */ void json_categorize_type(Oid typoid, bool is_jsonb, diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index 27c2d20610..27a25ba283 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -63,7 +63,7 @@ extern Jsonb *transform_jsonb_string_values(Jsonb *jsonb, void *action_state, extern text *transform_json_string_values(text *json, void *action_state, JsonTransformStringValuesAction transform_action); -/* Type categories for datum_to_json[b] and friends. */ +/* Type categories returned by json_categorize_type */ typedef enum { JSONTYPE_NULL, /* null, so we didn't bother to identify */ @@ -72,8 +72,8 @@ typedef enum JSONTYPE_DATE, /* we use special formatting for datetimes */ JSONTYPE_TIMESTAMP, JSONTYPE_TIMESTAMPTZ, - JSONTYPE_JSON, /* JSON and JSONB */ - JSONTYPE_JSONB, /* JSONB (for datum_to_jsonb) */ + JSONTYPE_JSON, /* JSON (and JSONB, if not is_jsonb) */ + JSONTYPE_JSONB, /* JSONB (if is_jsonb) */ JSONTYPE_ARRAY, /* array */ JSONTYPE_COMPOSITE, /* composite */ JSONTYPE_CAST, /* something with an explicit cast to JSON */ @@ -81,6 +81,6 @@ typedef enum } JsonTypeCategory; void json_categorize_type(Oid typoid, bool is_jsonb, - JsonTypeCategory *tcategory, Oid *outfuncoid); + JsonTypeCategory *tcategory, Oid *outfuncoid); #endif -- 2.30.2
0002-make-the-output-generally-usable-not-just-for-datum_.notpatch (text/plain, 2.3K) download From fd025b1482223370cc10e82029468fdd99932368 Mon Sep 17 00:00:00 2001 From: Alvaro Herrera <alvherre@alvh.no-ip.org> Date: Thu, 13 Jul 2023 18:06:46 +0200 Subject: [PATCH 2/2] make the output generally usable, not just for datum_to_json[b] --- src/backend/utils/adt/jsonfuncs.c | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index 764d48505b..a4bfa5e404 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -5705,16 +5705,10 @@ json_categorize_type(Oid typoid, bool is_jsonb, *outfuncoid = InvalidOid; - /* - * We need to get the output function for everything except date and - * timestamp types, booleans, array and composite types, json and jsonb, - * and non-builtin types where there's a cast to json. In this last case - * we return the oid of the cast function instead. - */ - switch (typoid) { case BOOLOID: + *outfuncoid = F_BOOLOUT; *tcategory = JSONTYPE_BOOL; break; @@ -5729,26 +5723,27 @@ json_categorize_type(Oid typoid, bool is_jsonb, break; case DATEOID: + *outfuncoid = F_DATE_OUT; *tcategory = JSONTYPE_DATE; break; case TIMESTAMPOID: + *outfuncoid = F_TIMESTAMP_OUT; *tcategory = JSONTYPE_TIMESTAMP; break; case TIMESTAMPTZOID: + *outfuncoid = F_TIMESTAMPTZ_OUT; *tcategory = JSONTYPE_TIMESTAMPTZ; break; case JSONOID: - if (!is_jsonb) - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); *tcategory = JSONTYPE_JSON; break; case JSONBOID: - if (!is_jsonb) - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); *tcategory = is_jsonb ? JSONTYPE_JSONB : JSONTYPE_JSON; break; @@ -5756,9 +5751,15 @@ json_categorize_type(Oid typoid, bool is_jsonb, /* Check for arrays and composites */ if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID || typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID) + { + *outfuncoid = F_ARRAY_OUT; *tcategory = JSONTYPE_ARRAY; + } else if (type_is_rowtype(typoid)) /* includes RECORDOID */ + { + *outfuncoid = F_RECORD_OUT; *tcategory = JSONTYPE_COMPOSITE; + } else { /* -- 2.30.2
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-14 07:13  Amit Langote <amitlangote09@gmail.com>
  parent: Alvaro Herrera <alvherre@alvh.no-ip.org>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-07-14 07:13 UTC (permalink / raw)
  To: Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: pgsql-hackers; Andrew Dunstan <andrew@dunslane.net>

Hi Alvaro,

On Fri, Jul 14, 2023 at 1:54 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
> I looked at your 0001.  My 0001 are some trivial comment cleanups to
> that.

Thanks.

> I scrolled through all of jsonfuncs.c to see if there was a better place
> for the new function than the end of the file.  Man, is that one ugly
> file.  There are almost no comments!  I almost wish you would create a
> new file so that you don't have to put this new function in such bad
> company.  But maybe it'll improve someday, so ... whatever.

I tried to put it somewhere that is not the end of the file, though
anywhere would have looked arbitrary anyway for the reasons you
mention, so I didn't after all.

> In the original code, the functions here being (re)moved do not need to
> return a type output function in a few cases.  This works okay when the
> functions are each contained in a single file (because each function
> knows that the respective datum_to_json/datum_to_jsonb user of the
> returned values won't need the function OID in those other cases); but
> as an exported function, that strange API doesn't seem great.  (It only
> works for 0002 because the only thing that the executor does with these
> cached values is call datum_to_json/b).

Agreed about not tying the new API too closely to datum_to_json[b]'s needs.

>  That seems easy to solve, since
> we can return the hardcoded output function OID in those cases anyway.
> A possible complaint about this is that the OID so returned would be
> untested code, so they might be wrong and we'd never know.  However,
> ISTM it's better to make a promise about always returning a function OID
> and later fixing any bogus function OID if we ever discover that we
> return one, rather than having to document in the function's comment
> that "we only return function OIDs in such and such cases".  So I made
> this change my 0002.

+1

> A similar complaint can be made about which casts we look for.  Right
> now, only an explicit cast to JSON is useful, so that's the only thing
> we do.  But maybe one day a cast to JSONB would become useful if there's
> no cast to JSON for some datatype (in the is_jsonb case only?); and
> maybe another type of cast would be useful.  However, that seems like
> going too much into uncharted territory with no useful use case, so
> let's just not go there for now.  Maybe in the future we can improve
> this aspect of it, if need arises.

Hmm, yes, the note in the nearby comment stresses "to json (not to
jsonb)", though the (historical) reason why is not so clear to me.
I'm inclined to leave that as-is.

I've merged your deltas in the attached 0001 and rebased the other
patches.  In 0002, I have now removed RETURNING support for JSON() and
JSON_SCALAR().


--
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

Attachments: download all (5 files, .tar.gz)

v7-0001-Unify-JSON-categorize-type-API-and-export-for-ext.patch (application/octet-stream, 27.6K) download From 081342b1573b01703d72a3267a066f38523e7e20 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Tue, 11 Jul 2023 16:00:42 +0900 Subject: [PATCH v7 1/5] Unify JSON categorize type API and export for external use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This essentially removes the JsonbTypeCategory enum and jsonb_categorize_type() and integrates any jsonb-specific logic that was in jsonb_categorize_type() into json_categorize_type(), now moved to jsonfuncs.c. The remaining JsonTypeCategory enum and json_categorize_type() cover the needs of the callers in both json.c and jsonb.c. json_categorize_type() has grown a new parameter named is_jsonb for callers to engage the jsonb-specific behavior of json_categorize_type(). One notable change in the now exported API of json_categorize_type() is that it now always returns *outfuncoid even though a caller may have no need currently to see one. Co-authored-by: Álvaro Herrera <alvherre@alvh.no-ip.org> Reviewed-by: Álvaro Herrera <alvherre@alvh.no-ip.org> Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- src/backend/utils/adt/json.c | 137 ++--------------- src/backend/utils/adt/jsonb.c | 245 +++++++----------------------- src/backend/utils/adt/jsonfuncs.c | 111 ++++++++++++++ src/include/utils/jsonfuncs.h | 20 +++ src/tools/pgindent/typedefs.list | 1 - 5 files changed, 199 insertions(+), 315 deletions(-) diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index 49080e5fbf..f6bef9c148 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -19,7 +19,6 @@ #include "funcapi.h" #include "libpq/pqformat.h" #include "miscadmin.h" -#include "parser/parse_coerce.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/date.h" @@ -29,21 +28,6 @@ #include "utils/lsyscache.h" #include "utils/typcache.h" -typedef enum /* type categories for datum_to_json */ -{ - JSONTYPE_NULL, /* null, so we didn't bother to identify */ - JSONTYPE_BOOL, /* boolean (built-in types only) */ - JSONTYPE_NUMERIC, /* numeric (ditto) */ - JSONTYPE_DATE, /* we use special formatting for datetimes */ - JSONTYPE_TIMESTAMP, - JSONTYPE_TIMESTAMPTZ, - JSONTYPE_JSON, /* JSON itself (and JSONB) */ - JSONTYPE_ARRAY, /* array */ - JSONTYPE_COMPOSITE, /* composite */ - JSONTYPE_CAST, /* something with an explicit cast to JSON */ - JSONTYPE_OTHER /* all else */ -} JsonTypeCategory; - /* * Support for fast key uniqueness checking. @@ -107,9 +91,6 @@ static void array_dim_to_json(StringInfo result, int dim, int ndims, int *dims, bool use_line_feeds); static void array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds); -static void json_categorize_type(Oid typoid, - JsonTypeCategory *tcategory, - Oid *outfuncoid); static void datum_to_json(Datum val, bool is_null, StringInfo result, JsonTypeCategory tcategory, Oid outfuncoid, bool key_scalar); @@ -182,106 +163,6 @@ json_recv(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(cstring_to_text_with_len(str, nbytes)); } -/* - * Determine how we want to print values of a given type in datum_to_json. - * - * Given the datatype OID, return its JsonTypeCategory, as well as the type's - * output function OID. If the returned category is JSONTYPE_CAST, we - * return the OID of the type->JSON cast function instead. - */ -static void -json_categorize_type(Oid typoid, - JsonTypeCategory *tcategory, - Oid *outfuncoid) -{ - bool typisvarlena; - - /* Look through any domain */ - typoid = getBaseType(typoid); - - *outfuncoid = InvalidOid; - - /* - * We need to get the output function for everything except date and - * timestamp types, array and composite types, booleans, and non-builtin - * types where there's a cast to json. - */ - - switch (typoid) - { - case BOOLOID: - *tcategory = JSONTYPE_BOOL; - break; - - case INT2OID: - case INT4OID: - case INT8OID: - case FLOAT4OID: - case FLOAT8OID: - case NUMERICOID: - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - *tcategory = JSONTYPE_NUMERIC; - break; - - case DATEOID: - *tcategory = JSONTYPE_DATE; - break; - - case TIMESTAMPOID: - *tcategory = JSONTYPE_TIMESTAMP; - break; - - case TIMESTAMPTZOID: - *tcategory = JSONTYPE_TIMESTAMPTZ; - break; - - case JSONOID: - case JSONBOID: - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - *tcategory = JSONTYPE_JSON; - break; - - default: - /* Check for arrays and composites */ - if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID - || typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID) - *tcategory = JSONTYPE_ARRAY; - else if (type_is_rowtype(typoid)) /* includes RECORDOID */ - *tcategory = JSONTYPE_COMPOSITE; - else - { - /* It's probably the general case ... */ - *tcategory = JSONTYPE_OTHER; - /* but let's look for a cast to json, if it's not built-in */ - if (typoid >= FirstNormalObjectId) - { - Oid castfunc; - CoercionPathType ctype; - - ctype = find_coercion_pathway(JSONOID, typoid, - COERCION_EXPLICIT, - &castfunc); - if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc)) - { - *tcategory = JSONTYPE_CAST; - *outfuncoid = castfunc; - } - else - { - /* non builtin type with no cast */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - } - else - { - /* any other builtin type */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - } - break; - } -} - /* * Turn a Datum into JSON text, appending the string to "result". * @@ -591,7 +472,7 @@ array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds) get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign); - json_categorize_type(element_type, + json_categorize_type(element_type, false, &tcategory, &outfuncoid); deconstruct_array(v, element_type, typlen, typbyval, @@ -665,7 +546,8 @@ composite_to_json(Datum composite, StringInfo result, bool use_line_feeds) outfuncoid = InvalidOid; } else - json_categorize_type(att->atttypid, &tcategory, &outfuncoid); + json_categorize_type(att->atttypid, false, &tcategory, + &outfuncoid); datum_to_json(val, isnull, result, tcategory, outfuncoid, false); } @@ -699,7 +581,7 @@ add_json(Datum val, bool is_null, StringInfo result, outfuncoid = InvalidOid; } else - json_categorize_type(val_type, + json_categorize_type(val_type, false, &tcategory, &outfuncoid); datum_to_json(val, is_null, result, tcategory, outfuncoid, key_scalar); @@ -784,12 +666,13 @@ to_json_is_immutable(Oid typoid) JsonTypeCategory tcategory; Oid outfuncoid; - json_categorize_type(typoid, &tcategory, &outfuncoid); + json_categorize_type(typoid, false, &tcategory, &outfuncoid); switch (tcategory) { case JSONTYPE_BOOL: case JSONTYPE_JSON: + case JSONTYPE_JSONB: case JSONTYPE_NULL: return true; @@ -830,7 +713,7 @@ to_json(PG_FUNCTION_ARGS) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - json_categorize_type(val_type, + json_categorize_type(val_type, false, &tcategory, &outfuncoid); result = makeStringInfo(); @@ -880,7 +763,7 @@ json_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null) MemoryContextSwitchTo(oldcontext); appendStringInfoChar(state->str, '['); - json_categorize_type(arg_type, &state->val_category, + json_categorize_type(arg_type, false, &state->val_category, &state->val_output_func); } else @@ -1112,7 +995,7 @@ json_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine data type for argument %d", 1))); - json_categorize_type(arg_type, &state->key_category, + json_categorize_type(arg_type, false, &state->key_category, &state->key_output_func); arg_type = get_fn_expr_argtype(fcinfo->flinfo, 2); @@ -1122,7 +1005,7 @@ json_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine data type for argument %d", 2))); - json_categorize_type(arg_type, &state->val_category, + json_categorize_type(arg_type, false, &state->val_category, &state->val_output_func); appendStringInfoString(state->str, "{ "); diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index cf43c3f2de..fc64f56868 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -19,7 +19,6 @@ #include "funcapi.h" #include "libpq/pqformat.h" #include "miscadmin.h" -#include "parser/parse_coerce.h" #include "utils/builtins.h" #include "utils/date.h" #include "utils/datetime.h" @@ -37,29 +36,12 @@ typedef struct JsonbInState Node *escontext; } JsonbInState; -/* unlike with json categories, we need to treat json and jsonb differently */ -typedef enum /* type categories for datum_to_jsonb */ -{ - JSONBTYPE_NULL, /* null, so we didn't bother to identify */ - JSONBTYPE_BOOL, /* boolean (built-in types only) */ - JSONBTYPE_NUMERIC, /* numeric (ditto) */ - JSONBTYPE_DATE, /* we use special formatting for datetimes */ - JSONBTYPE_TIMESTAMP, /* we use special formatting for timestamp */ - JSONBTYPE_TIMESTAMPTZ, /* ... and timestamptz */ - JSONBTYPE_JSON, /* JSON */ - JSONBTYPE_JSONB, /* JSONB */ - JSONBTYPE_ARRAY, /* array */ - JSONBTYPE_COMPOSITE, /* composite */ - JSONBTYPE_JSONCAST, /* something with an explicit cast to JSON */ - JSONBTYPE_OTHER /* all else */ -} JsonbTypeCategory; - typedef struct JsonbAggState { JsonbInState *res; - JsonbTypeCategory key_category; + JsonTypeCategory key_category; Oid key_output_func; - JsonbTypeCategory val_category; + JsonTypeCategory val_category; Oid val_output_func; } JsonbAggState; @@ -72,19 +54,13 @@ static JsonParseErrorType jsonb_in_array_end(void *pstate); static JsonParseErrorType jsonb_in_object_field_start(void *pstate, char *fname, bool isnull); static void jsonb_put_escaped_value(StringInfo out, JsonbValue *scalarVal); static JsonParseErrorType jsonb_in_scalar(void *pstate, char *token, JsonTokenType tokentype); -static void jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid); static void composite_to_jsonb(Datum composite, JsonbInState *result); static void array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *dims, Datum *vals, bool *nulls, int *valcount, - JsonbTypeCategory tcategory, Oid outfuncoid); + JsonTypeCategory tcategory, Oid outfuncoid); static void array_to_jsonb_internal(Datum array, JsonbInState *result); -static void jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid); static void datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, - JsonbTypeCategory tcategory, Oid outfuncoid, + JsonTypeCategory tcategory, Oid outfuncoid, bool key_scalar); static void add_jsonb(Datum val, bool is_null, JsonbInState *result, Oid val_type, bool key_scalar); @@ -633,112 +609,6 @@ add_indent(StringInfo out, bool indent, int level) } -/* - * Determine how we want to render values of a given type in datum_to_jsonb. - * - * Given the datatype OID, return its JsonbTypeCategory, as well as the type's - * output function OID. If the returned category is JSONBTYPE_JSONCAST, - * we return the OID of the relevant cast function instead. - */ -static void -jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid) -{ - bool typisvarlena; - - /* Look through any domain */ - typoid = getBaseType(typoid); - - *outfuncoid = InvalidOid; - - /* - * We need to get the output function for everything except date and - * timestamp types, booleans, array and composite types, json and jsonb, - * and non-builtin types where there's a cast to json. In this last case - * we return the oid of the cast function instead. - */ - - switch (typoid) - { - case BOOLOID: - *tcategory = JSONBTYPE_BOOL; - break; - - case INT2OID: - case INT4OID: - case INT8OID: - case FLOAT4OID: - case FLOAT8OID: - case NUMERICOID: - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - *tcategory = JSONBTYPE_NUMERIC; - break; - - case DATEOID: - *tcategory = JSONBTYPE_DATE; - break; - - case TIMESTAMPOID: - *tcategory = JSONBTYPE_TIMESTAMP; - break; - - case TIMESTAMPTZOID: - *tcategory = JSONBTYPE_TIMESTAMPTZ; - break; - - case JSONBOID: - *tcategory = JSONBTYPE_JSONB; - break; - - case JSONOID: - *tcategory = JSONBTYPE_JSON; - break; - - default: - /* Check for arrays and composites */ - if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID - || typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID) - *tcategory = JSONBTYPE_ARRAY; - else if (type_is_rowtype(typoid)) /* includes RECORDOID */ - *tcategory = JSONBTYPE_COMPOSITE; - else - { - /* It's probably the general case ... */ - *tcategory = JSONBTYPE_OTHER; - - /* - * but first let's look for a cast to json (note: not to - * jsonb) if it's not built-in. - */ - if (typoid >= FirstNormalObjectId) - { - Oid castfunc; - CoercionPathType ctype; - - ctype = find_coercion_pathway(JSONOID, typoid, - COERCION_EXPLICIT, &castfunc); - if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc)) - { - *tcategory = JSONBTYPE_JSONCAST; - *outfuncoid = castfunc; - } - else - { - /* not a cast type, so just get the usual output func */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - } - else - { - /* any other builtin type */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - break; - } - } -} - /* * Turn a Datum into jsonb, adding it to the result JsonbInState. * @@ -753,7 +623,7 @@ jsonb_categorize_type(Oid typoid, */ static void datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, - JsonbTypeCategory tcategory, Oid outfuncoid, + JsonTypeCategory tcategory, Oid outfuncoid, bool key_scalar) { char *outputstr; @@ -770,11 +640,11 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, jb.type = jbvNull; } else if (key_scalar && - (tcategory == JSONBTYPE_ARRAY || - tcategory == JSONBTYPE_COMPOSITE || - tcategory == JSONBTYPE_JSON || - tcategory == JSONBTYPE_JSONB || - tcategory == JSONBTYPE_JSONCAST)) + (tcategory == JSONTYPE_ARRAY || + tcategory == JSONTYPE_COMPOSITE || + tcategory == JSONTYPE_JSON || + tcategory == JSONTYPE_JSONB || + tcategory == JSONTYPE_JSON)) { ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -782,18 +652,18 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, } else { - if (tcategory == JSONBTYPE_JSONCAST) + if (tcategory == JSONTYPE_CAST) val = OidFunctionCall1(outfuncoid, val); switch (tcategory) { - case JSONBTYPE_ARRAY: + case JSONTYPE_ARRAY: array_to_jsonb_internal(val, result); break; - case JSONBTYPE_COMPOSITE: + case JSONTYPE_COMPOSITE: composite_to_jsonb(val, result); break; - case JSONBTYPE_BOOL: + case JSONTYPE_BOOL: if (key_scalar) { outputstr = DatumGetBool(val) ? "true" : "false"; @@ -807,7 +677,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, jb.val.boolean = DatumGetBool(val); } break; - case JSONBTYPE_NUMERIC: + case JSONTYPE_NUMERIC: outputstr = OidOutputFunctionCall(outfuncoid, val); if (key_scalar) { @@ -845,26 +715,26 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, } } break; - case JSONBTYPE_DATE: + case JSONTYPE_DATE: jb.type = jbvString; jb.val.string.val = JsonEncodeDateTime(NULL, val, DATEOID, NULL); jb.val.string.len = strlen(jb.val.string.val); break; - case JSONBTYPE_TIMESTAMP: + case JSONTYPE_TIMESTAMP: jb.type = jbvString; jb.val.string.val = JsonEncodeDateTime(NULL, val, TIMESTAMPOID, NULL); jb.val.string.len = strlen(jb.val.string.val); break; - case JSONBTYPE_TIMESTAMPTZ: + case JSONTYPE_TIMESTAMPTZ: jb.type = jbvString; jb.val.string.val = JsonEncodeDateTime(NULL, val, TIMESTAMPTZOID, NULL); jb.val.string.len = strlen(jb.val.string.val); break; - case JSONBTYPE_JSONCAST: - case JSONBTYPE_JSON: + case JSONTYPE_CAST: + case JSONTYPE_JSON: { /* parse the json right into the existing result object */ JsonLexContext *lex; @@ -887,7 +757,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, pg_parse_json_or_ereport(lex, &sem); } break; - case JSONBTYPE_JSONB: + case JSONTYPE_JSONB: { Jsonb *jsonb = DatumGetJsonbP(val); JsonbIterator *it; @@ -931,7 +801,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, /* Now insert jb into result, unless we did it recursively */ if (!is_null && !scalar_jsonb && - tcategory >= JSONBTYPE_JSON && tcategory <= JSONBTYPE_JSONCAST) + tcategory >= JSONTYPE_JSON && tcategory <= JSONTYPE_CAST) { /* work has been done recursively */ return; @@ -976,7 +846,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, */ static void array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *dims, Datum *vals, - bool *nulls, int *valcount, JsonbTypeCategory tcategory, + bool *nulls, int *valcount, JsonTypeCategory tcategory, Oid outfuncoid) { int i; @@ -1020,7 +890,7 @@ array_to_jsonb_internal(Datum array, JsonbInState *result) int16 typlen; bool typbyval; char typalign; - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; ndim = ARR_NDIM(v); @@ -1037,8 +907,8 @@ array_to_jsonb_internal(Datum array, JsonbInState *result) get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign); - jsonb_categorize_type(element_type, - &tcategory, &outfuncoid); + json_categorize_type(element_type, true, + &tcategory, &outfuncoid); deconstruct_array(v, element_type, typlen, typbyval, typalign, &elements, &nulls, @@ -1084,7 +954,7 @@ composite_to_jsonb(Datum composite, JsonbInState *result) Datum val; bool isnull; char *attname; - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; JsonbValue v; Form_pg_attribute att = TupleDescAttr(tupdesc, i); @@ -1105,11 +975,12 @@ composite_to_jsonb(Datum composite, JsonbInState *result) if (isnull) { - tcategory = JSONBTYPE_NULL; + tcategory = JSONTYPE_NULL; outfuncoid = InvalidOid; } else - jsonb_categorize_type(att->atttypid, &tcategory, &outfuncoid); + json_categorize_type(att->atttypid, true, &tcategory, + &outfuncoid); datum_to_jsonb(val, isnull, result, tcategory, outfuncoid, false); } @@ -1122,7 +993,7 @@ composite_to_jsonb(Datum composite, JsonbInState *result) * Append JSON text for "val" to "result". * * This is just a thin wrapper around datum_to_jsonb. If the same type will be - * printed many times, avoid using this; better to do the jsonb_categorize_type + * printed many times, avoid using this; better to do the json_categorize_type * lookups only once. */ @@ -1130,7 +1001,7 @@ static void add_jsonb(Datum val, bool is_null, JsonbInState *result, Oid val_type, bool key_scalar) { - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; if (val_type == InvalidOid) @@ -1140,12 +1011,12 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, if (is_null) { - tcategory = JSONBTYPE_NULL; + tcategory = JSONTYPE_NULL; outfuncoid = InvalidOid; } else - jsonb_categorize_type(val_type, - &tcategory, &outfuncoid); + json_categorize_type(val_type, true, + &tcategory, &outfuncoid); datum_to_jsonb(val, is_null, result, tcategory, outfuncoid, key_scalar); } @@ -1160,33 +1031,33 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, bool to_jsonb_is_immutable(Oid typoid) { - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; - jsonb_categorize_type(typoid, &tcategory, &outfuncoid); + json_categorize_type(typoid, true, &tcategory, &outfuncoid); switch (tcategory) { - case JSONBTYPE_NULL: - case JSONBTYPE_BOOL: - case JSONBTYPE_JSON: - case JSONBTYPE_JSONB: + case JSONTYPE_NULL: + case JSONTYPE_BOOL: + case JSONTYPE_JSON: + case JSONTYPE_JSONB: return true; - case JSONBTYPE_DATE: - case JSONBTYPE_TIMESTAMP: - case JSONBTYPE_TIMESTAMPTZ: + case JSONTYPE_DATE: + case JSONTYPE_TIMESTAMP: + case JSONTYPE_TIMESTAMPTZ: return false; - case JSONBTYPE_ARRAY: + case JSONTYPE_ARRAY: return false; /* TODO recurse into elements */ - case JSONBTYPE_COMPOSITE: + case JSONTYPE_COMPOSITE: return false; /* TODO recurse into fields */ - case JSONBTYPE_NUMERIC: - case JSONBTYPE_JSONCAST: - case JSONBTYPE_OTHER: + case JSONTYPE_NUMERIC: + case JSONTYPE_CAST: + case JSONTYPE_OTHER: return func_volatile(outfuncoid) == PROVOLATILE_IMMUTABLE; } @@ -1202,7 +1073,7 @@ to_jsonb(PG_FUNCTION_ARGS) Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); JsonbInState result; - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; if (val_type == InvalidOid) @@ -1210,8 +1081,8 @@ to_jsonb(PG_FUNCTION_ARGS) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - jsonb_categorize_type(val_type, - &tcategory, &outfuncoid); + json_categorize_type(val_type, true, + &tcategory, &outfuncoid); memset(&result, 0, sizeof(JsonbInState)); @@ -1636,8 +1507,8 @@ jsonb_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null) WJB_BEGIN_ARRAY, NULL); MemoryContextSwitchTo(oldcontext); - jsonb_categorize_type(arg_type, &state->val_category, - &state->val_output_func); + json_categorize_type(arg_type, true, &state->val_category, + &state->val_output_func); } else { @@ -1816,8 +1687,8 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - jsonb_categorize_type(arg_type, &state->key_category, - &state->key_output_func); + json_categorize_type(arg_type, true, &state->key_category, + &state->key_output_func); arg_type = get_fn_expr_argtype(fcinfo->flinfo, 2); @@ -1826,8 +1697,8 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - jsonb_categorize_type(arg_type, &state->val_category, - &state->val_output_func); + json_categorize_type(arg_type, true, &state->val_category, + &state->val_output_func); } else { diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index 70cb922e6b..a4bfa5e404 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -26,6 +26,7 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" #include "nodes/miscnodes.h" +#include "parser/parse_coerce.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -5685,3 +5686,113 @@ json_get_first_token(text *json, bool throw_error) return JSON_TOKEN_INVALID; /* invalid json */ } + +/* + * Determine how we want to print values of a given type in datum_to_json(b). + * + * Given the datatype OID, return its JsonTypeCategory, as well as the type's + * output function OID. If the returned category is JSONTYPE_CAST, we return + * the OID of the type->JSON cast function instead. + */ +void +json_categorize_type(Oid typoid, bool is_jsonb, + JsonTypeCategory *tcategory, Oid *outfuncoid) +{ + bool typisvarlena; + + /* Look through any domain */ + typoid = getBaseType(typoid); + + *outfuncoid = InvalidOid; + + switch (typoid) + { + case BOOLOID: + *outfuncoid = F_BOOLOUT; + *tcategory = JSONTYPE_BOOL; + break; + + case INT2OID: + case INT4OID: + case INT8OID: + case FLOAT4OID: + case FLOAT8OID: + case NUMERICOID: + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + *tcategory = JSONTYPE_NUMERIC; + break; + + case DATEOID: + *outfuncoid = F_DATE_OUT; + *tcategory = JSONTYPE_DATE; + break; + + case TIMESTAMPOID: + *outfuncoid = F_TIMESTAMP_OUT; + *tcategory = JSONTYPE_TIMESTAMP; + break; + + case TIMESTAMPTZOID: + *outfuncoid = F_TIMESTAMPTZ_OUT; + *tcategory = JSONTYPE_TIMESTAMPTZ; + break; + + case JSONOID: + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + *tcategory = JSONTYPE_JSON; + break; + + case JSONBOID: + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + *tcategory = is_jsonb ? JSONTYPE_JSONB : JSONTYPE_JSON; + break; + + default: + /* Check for arrays and composites */ + if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID + || typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID) + { + *outfuncoid = F_ARRAY_OUT; + *tcategory = JSONTYPE_ARRAY; + } + else if (type_is_rowtype(typoid)) /* includes RECORDOID */ + { + *outfuncoid = F_RECORD_OUT; + *tcategory = JSONTYPE_COMPOSITE; + } + else + { + /* + * It's probably the general case. But let's look for a cast + * to json (note: not to jsonb even if is_jsonb is true), if + * it's not built-in. + */ + *tcategory = JSONTYPE_OTHER; + if (typoid >= FirstNormalObjectId) + { + Oid castfunc; + CoercionPathType ctype; + + ctype = find_coercion_pathway(JSONOID, typoid, + COERCION_EXPLICIT, + &castfunc); + if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc)) + { + *outfuncoid = castfunc; + *tcategory = JSONTYPE_CAST; + } + else + { + /* non builtin type with no cast */ + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + } + } + else + { + /* any other builtin type */ + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + } + } + break; + } +} diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index a85203d4a4..27a25ba283 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -63,4 +63,24 @@ extern Jsonb *transform_jsonb_string_values(Jsonb *jsonb, void *action_state, extern text *transform_json_string_values(text *json, void *action_state, JsonTransformStringValuesAction transform_action); +/* Type categories returned by json_categorize_type */ +typedef enum +{ + JSONTYPE_NULL, /* null, so we didn't bother to identify */ + JSONTYPE_BOOL, /* boolean (built-in types only) */ + JSONTYPE_NUMERIC, /* numeric (ditto) */ + JSONTYPE_DATE, /* we use special formatting for datetimes */ + JSONTYPE_TIMESTAMP, + JSONTYPE_TIMESTAMPTZ, + JSONTYPE_JSON, /* JSON (and JSONB, if not is_jsonb) */ + JSONTYPE_JSONB, /* JSONB (if is_jsonb) */ + JSONTYPE_ARRAY, /* array */ + JSONTYPE_COMPOSITE, /* composite */ + JSONTYPE_CAST, /* something with an explicit cast to JSON */ + JSONTYPE_OTHER, /* all else */ +} JsonTypeCategory; + +void json_categorize_type(Oid typoid, bool is_jsonb, + JsonTypeCategory *tcategory, Oid *outfuncoid); + #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e941fb6c82..b10590a252 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1318,7 +1318,6 @@ JsonbIteratorToken JsonbPair JsonbParseState JsonbSubWorkspace -JsonbTypeCategory JsonbValue JumbleState JunkFilter -- 2.35.3
v7-0002-SQL-JSON-functions.patch (application/octet-stream, 50.9K) download From 8a6fefbc9efafb3ee5c73ebf6310946627d1bca5 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:12:39 +0900 Subject: [PATCH v7 2/5] SQL JSON functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This Patch introduces three SQL standard JSON functions: JSON() JSON_SCALAR() JSON_SERIALIZE() JSON() produces json values from text, bytea, json or jsonb values, and has facilitites for handling duplicate keys. JSON_SCALAR() produces a json value from any scalar sql value, including json and jsonb. JSON_SERIALIZE() produces text or bytea from input which containis or represents json or jsonb; For the most part these functions don't add any significant new capabilities, but they will be of use to users wanting standard compliant JSON handling. Author: Nikita Glukhov <n.gluhov@postgrespro.ru> Author: Teodor Sigaev <teodor@sigaev.ru> Author: Oleg Bartunov <obartunov@gmail.com> Author: Alexander Korotkov <aekorotkov@gmail.com> Author: Andrew Dunstan <andrew@dunslane.net> Author: Amit Langote <amitlangote09@gmail.com> Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- doc/src/sgml/func.sgml | 71 ++++++ src/backend/executor/execExpr.c | 30 +++ src/backend/executor/execExprInterp.c | 43 +++- src/backend/nodes/nodeFuncs.c | 30 +++ src/backend/parser/gram.y | 68 +++++- src/backend/parser/parse_expr.c | 208 ++++++++++++++-- src/backend/parser/parse_target.c | 9 + src/backend/utils/adt/format_type.c | 4 + src/backend/utils/adt/json.c | 21 +- src/backend/utils/adt/jsonb.c | 48 +++- src/backend/utils/adt/ruleutils.c | 14 +- src/include/nodes/parsenodes.h | 48 ++++ src/include/nodes/primnodes.h | 5 +- src/include/parser/kwlist.h | 4 +- src/include/utils/jsonb.h | 2 +- src/include/utils/jsonfuncs.h | 4 + src/test/regress/expected/sqljson.out | 332 ++++++++++++++++++++++++++ src/test/regress/sql/sqljson.sql | 85 +++++++ src/tools/pgindent/typedefs.list | 1 + 19 files changed, 978 insertions(+), 49 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 0b62e0c828..9cece06c18 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16001,6 +16001,77 @@ table2-mapping <returnvalue>{"a": "1", "b": "2"}</returnvalue> </para></entry> </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json constructor</primary></indexterm> + <function>json</function> ( + <replaceable>expression</replaceable> + <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional></optional> + <optional> { <literal>WITH</literal> | <literal>WITHOUT</literal> } <literal>UNIQUE</literal> <optional> <literal>KEYS</literal> </optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>json_data_type</replaceable> </optional>) + </para> + <para> + The <replaceable>expression</replaceable> can be any text type or a + <type>bytea</type> in UTF8 encoding. If the + <replaceable>expression</replaceable> is NULL, an + <acronym>SQL</acronym> null value is returned. + If <literal>WITH UNIQUE</literal> is specified, the + <replaceable>expression</replaceable> must not contain any duplicate + object keys. + </para> + <para> + <literal>json('{"a":123, "b":[true,"foo"], "a":"bar"}')</literal> + <returnvalue>{"a":123, "b":[true,"foo"], "a":"bar"}</returnvalue> + </para> + <para> + <literal>json('{"a":123,"b":[true,"foo"],"a":"bar"}' returning jsonb)</literal> + <returnvalue>{"a": "bar", "b": [true, "foo"]}</returnvalue> + </para> + </entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json_scalar</primary></indexterm> + <function>json_scalar</function> (<replaceable>expression</replaceable>) + <optional> <literal>RETURNING</literal> <replaceable>json_data_type</replaceable> </optional>) + </para> + <para> + Returns a JSON scalar value representing + <replaceable>expression</replaceable>. + If the input is NULL, an SQL NULL is returned. If the input is a number + or a boolean value, a corresponding JSON number or boolean value is + returned. For any other value a JSON string is returned. + </para> + <para> + <literal>json_scalar(123.45)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_scalar(CURRENT_TIMESTAMP)</literal> + <returnvalue>"2022-05-10T10:51:04.62128-04:00"</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <function>json_serialize</function> ( + <replaceable>expression</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional>) + </para> + <para> + Transforms an SQL/JSON value into a character or binary string. The + <replaceable>expression</replaceable> can be of any JSON type, any + character string type, or <type>bytea</type> in UTF8 encoding. + The returned type can be any character string type or + <type>bytea</type>. The default is <type>text</type>. + </para> + <para> + <literal>json_serialize('{ "a" : 1 } ' RETURNING bytea)</literal> + <returnvalue>\x7b20226122203a2031207d20</returnvalue> + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index bf3a08c5f0..6ca4098bef 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -48,6 +48,7 @@ #include "utils/array.h" #include "utils/builtins.h" #include "utils/datum.h" +#include "utils/jsonfuncs.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -2311,6 +2312,12 @@ ExecInitExprRec(Expr *node, ExprState *state, { ExecInitExprRec(ctor->func, state, resv, resnull); } + else if ((ctor->type == JSCTOR_JSON_PARSE && !ctor->unique) || + ctor->type == JSCTOR_JSON_SERIALIZE) + { + /* Use the value of the first argument as a result */ + ExecInitExprRec(linitial(args), state, resv, resnull); + } else { JsonConstructorExprState *jcstate; @@ -2349,6 +2356,29 @@ ExecInitExprRec(Expr *node, ExprState *state, argno++; } + /* prepare type cache for datum_to_json[b]() */ + if (ctor->type == JSCTOR_JSON_SCALAR) + { + bool is_jsonb = + ctor->returning->format->format_type == JS_FORMAT_JSONB; + + jcstate->arg_type_cache = + palloc(sizeof(*jcstate->arg_type_cache) * nargs); + + for (int i = 0; i < nargs; i++) + { + JsonTypeCategory category; + Oid outfuncid; + Oid typid = jcstate->arg_types[i]; + + json_categorize_type(typid, is_jsonb, + &category, &outfuncid); + + jcstate->arg_type_cache[i].outfuncid = outfuncid; + jcstate->arg_type_cache[i].category = (int) category; + } + } + ExprEvalPushStep(state, &scratch); } diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 851946a927..76e59691e5 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -3992,7 +3992,7 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_values, jcstate->arg_nulls, jcstate->arg_types, - jcstate->constructor->absent_on_null); + ctor->absent_on_null); else if (ctor->type == JSCTOR_JSON_OBJECT) res = (is_jsonb ? jsonb_build_object_worker : @@ -4002,6 +4002,47 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_types, jcstate->constructor->absent_on_null, jcstate->constructor->unique); + else if (ctor->type == JSCTOR_JSON_SCALAR) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + Oid outfuncid = jcstate->arg_type_cache[0].outfuncid; + JsonTypeCategory category = (JsonTypeCategory) + jcstate->arg_type_cache[0].category; + + if (is_jsonb) + res = to_jsonb_worker(value, category, outfuncid); + else + res = to_json_worker(value, category, outfuncid); + } + } + else if (ctor->type == JSCTOR_JSON_PARSE) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + text *js = DatumGetTextP(value); + + if (is_jsonb) + res = jsonb_from_text(js, true); + else + { + (void) json_validate(js, true, true); + res = value; + } + } + } else elog(ERROR, "invalid JsonConstructorExpr type %d", ctor->type); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index c41e6bb984..dda964bd19 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -3901,6 +3901,36 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonParseExpr: + { + JsonParseExpr *jpe = (JsonParseExpr *) node; + + if (WALK(jpe->expr)) + return true; + if (WALK(jpe->output)) + return true; + } + break; + case T_JsonScalarExpr: + { + JsonScalarExpr *jse = (JsonScalarExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; + case T_JsonSerializeExpr: + { + JsonSerializeExpr *jse = (JsonSerializeExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; case T_JsonConstructorExpr: { JsonConstructorExpr *ctor = (JsonConstructorExpr *) node; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index edb6c00ece..26cbebb707 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -566,7 +566,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <list> copy_options %type <typnam> Typename SimpleTypename ConstTypename - GenericType Numeric opt_float + GenericType Numeric opt_float JsonType Character ConstCharacter CharacterWithLength CharacterWithoutLength ConstDatetime ConstInterval @@ -647,7 +647,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> json_format_clause_opt json_value_expr - json_output_clause_opt + json_returning_clause_opt json_name_and_value json_aggregate_func %type <list> json_name_and_value_list @@ -659,7 +659,6 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt - /* * Non-keyword token types. These are hard-wired into the "flex" lexer. * They must be listed first so that their numeric codes do not depend on @@ -723,6 +722,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG + JSON_SCALAR JSON_SERIALIZE KEY KEYS @@ -13981,6 +13981,7 @@ SimpleTypename: $$->typmods = list_make2(makeIntConst(INTERVAL_FULL_RANGE, -1), makeIntConst($3, @3)); } + | JsonType { $$ = $1; } ; /* We have a separate ConstTypename to allow defaulting fixed-length @@ -13999,6 +14000,7 @@ ConstTypename: | ConstBit { $$ = $1; } | ConstCharacter { $$ = $1; } | ConstDatetime { $$ = $1; } + | JsonType { $$ = $1; } ; /* @@ -14367,6 +14369,13 @@ interval_second: } ; +JsonType: + JSON + { + $$ = SystemTypeName("json"); + $$->location = @1; + } + ; /***************************************************************************** * @@ -15561,7 +15570,7 @@ func_expr_common_subexpr: | JSON_OBJECT '(' json_name_and_value_list json_object_constructor_null_clause_opt json_key_uniqueness_constraint_opt - json_output_clause_opt ')' + json_returning_clause_opt ')' { JsonObjectConstructor *n = makeNode(JsonObjectConstructor); @@ -15572,7 +15581,7 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } - | JSON_OBJECT '(' json_output_clause_opt ')' + | JSON_OBJECT '(' json_returning_clause_opt ')' { JsonObjectConstructor *n = makeNode(JsonObjectConstructor); @@ -15586,7 +15595,7 @@ func_expr_common_subexpr: | JSON_ARRAY '(' json_value_expr_list json_array_constructor_null_clause_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayConstructor *n = makeNode(JsonArrayConstructor); @@ -15601,7 +15610,7 @@ func_expr_common_subexpr: select_no_parens json_format_clause_opt /* json_array_constructor_null_clause_opt */ - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayQueryConstructor *n = makeNode(JsonArrayQueryConstructor); @@ -15614,7 +15623,7 @@ func_expr_common_subexpr: $$ = (Node *) n; } | JSON_ARRAY '(' - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayConstructor *n = makeNode(JsonArrayConstructor); @@ -15625,7 +15634,36 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } - ; + | JSON '(' json_value_expr json_key_uniqueness_constraint_opt ')' + { + JsonParseExpr *n = makeNode(JsonParseExpr); + + n->expr = (JsonValueExpr *) $3; + n->unique_keys = $4; + n->output = NULL; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SCALAR '(' a_expr ')' + { + JsonScalarExpr *n = makeNode(JsonScalarExpr); + + n->expr = (Expr *) $3; + n->output = NULL; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SERIALIZE '(' json_value_expr json_returning_clause_opt ')' + { + JsonSerializeExpr *n = makeNode(JsonSerializeExpr); + + n->expr = (JsonValueExpr *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + ; + /* * SQL/XML support @@ -16373,7 +16411,7 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; -json_output_clause_opt: +json_returning_clause_opt: RETURNING Typename json_format_clause_opt { JsonOutput *n = makeNode(JsonOutput); @@ -16446,7 +16484,7 @@ json_aggregate_func: json_name_and_value json_object_constructor_null_clause_opt json_key_uniqueness_constraint_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonObjectAgg *n = makeNode(JsonObjectAgg); @@ -16464,7 +16502,7 @@ json_aggregate_func: json_value_expr json_array_aggregate_order_by_clause_opt json_array_constructor_null_clause_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayAgg *n = makeNode(JsonArrayAgg); @@ -17064,7 +17102,6 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION - | JSON | KEY | KEYS | LABEL @@ -17279,10 +17316,13 @@ col_name_keyword: | INT_P | INTEGER | INTERVAL + | JSON | JSON_ARRAY | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | LEAST | NATIONAL | NCHAR @@ -17643,6 +17683,8 @@ bare_label_keyword: | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | KEY | KEYS | LABEL diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 5a05caa874..a2644dbc77 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -86,6 +86,10 @@ static Node *transformJsonArrayQueryConstructor(ParseState *pstate, static Node *transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg); static Node *transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg); static Node *transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred); +static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); +static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); +static Node *transformJsonSerializeExpr(ParseState *pstate, + JsonSerializeExpr * expr); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -337,6 +341,18 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonIsPredicate(pstate, (JsonIsPredicate *) expr); break; + case T_JsonParseExpr: + result = transformJsonParseExpr(pstate, (JsonParseExpr *) expr); + break; + + case T_JsonScalarExpr: + result = transformJsonScalarExpr(pstate, (JsonScalarExpr *) expr); + break; + + case T_JsonSerializeExpr: + result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3208,7 +3224,8 @@ makeJsonByteaToTextConversion(Node *expr, JsonFormat *format, int location) */ static Node * transformJsonValueExpr(ParseState *pstate, const char *constructName, - JsonValueExpr *ve, JsonFormatType default_format) + JsonValueExpr *ve, JsonFormatType default_format, + Oid targettype) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3250,12 +3267,14 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, else format = default_format; - if (format != JS_FORMAT_DEFAULT) + if (format != JS_FORMAT_DEFAULT || + (OidIsValid(targettype) && exprtype != targettype)) { - Oid targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; Node *coerced; + bool cast_is_needed = OidIsValid(targettype); - if (exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) + if (!cast_is_needed && + exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), errmsg(ve->format->format_type == JS_FORMAT_DEFAULT ? @@ -3271,6 +3290,9 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, exprtype = TEXTOID; } + if (!OidIsValid(targettype)) + targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; + /* Try to coerce to the target type. */ coerced = coerce_to_target_type(pstate, expr, exprtype, targettype, -1, @@ -3281,11 +3303,20 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, if (!coerced) { /* If coercion failed, use to_json()/to_jsonb() functions. */ - Oid fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; - FuncExpr *fexpr = makeFuncExpr(fnoid, targettype, - list_make1(expr), - InvalidOid, InvalidOid, - COERCE_EXPLICIT_CALL); + FuncExpr *fexpr; + Oid fnoid; + + if (cast_is_needed) /* only CAST is allowed */ + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(exprtype), + format_type_be(targettype)), + parser_errposition(pstate, location))); + + fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; + fexpr = makeFuncExpr(fnoid, targettype, list_make1(expr), + InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL); fexpr->location = location; @@ -3582,7 +3613,8 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) Node *key = transformExprRecurse(pstate, (Node *) kv->key); Node *val = transformJsonValueExpr(pstate, "JSON_OBJECT()", kv->value, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, key); args = lappend(args, val); @@ -3767,9 +3799,8 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) Oid aggtype; key = transformExprRecurse(pstate, (Node *) agg->arg->key); - val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", - agg->arg->value, - JS_FORMAT_DEFAULT); + val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", agg->arg->value, + JS_FORMAT_DEFAULT, InvalidOid); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3827,7 +3858,7 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", agg->arg, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, InvalidOid); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3875,7 +3906,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, "JSON_ARRAY()", jsval, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, val); } @@ -3958,3 +3990,149 @@ transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred) return makeJsonIsPredicate(expr, NULL, pred->item_type, pred->unique_keys, pred->location); } + +/* + * Transform the output clause of a JSON_*() expression if there is one and + * create one if not. + */ +static JsonReturning * +transformJsonReturning(ParseState *pstate, JsonOutput *output, const char *fname) +{ + JsonReturning *returning; + + if (output) + { + returning = transformJsonOutput(pstate, output, false); + + Assert(OidIsValid(returning->typid)); + + if (returning->typid != JSONOID && returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), fname), + parser_errposition(pstate, output->typeName->location))); + } + else + { + /* Output type is JSON by default. */ + Oid targettype = JSONOID; + JsonFormatType format = JS_FORMAT_JSON; + + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(format, JS_ENC_DEFAULT, -1); + returning->typid = targettype; + returning->typmod = -1; + } + + return returning; +} + +/* + * Transform a JSON() expression. + */ +static Node * +transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) +{ + JsonOutput *output = jsexpr->output; + JsonReturning *returning; + Node *arg; + + returning = transformJsonReturning(pstate, output, "JSON()"); + + if (jsexpr->unique_keys) + { + /* + * Coerce string argument to text and then to json[b] in the executor + * node with key uniqueness check. + */ + JsonValueExpr *jve = jsexpr->expr; + Oid arg_type; + + arg = transformJsonParseArg(pstate, (Node *) jve->raw_expr, jve->format, + &arg_type); + + if (arg_type != TEXTOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use non-string types with WITH UNIQUE KEYS clause"), + parser_errposition(pstate, jsexpr->location))); + } + else + { + /* + * Coerce argument to target type using CAST for compatibility with PG + * function-like CASTs. + */ + arg = transformJsonValueExpr(pstate, "JSON()", jsexpr->expr, + JS_FORMAT_JSON, returning->typid); + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, + returning, jsexpr->unique_keys, false, + jsexpr->location); +} + +/* + * Transform a JSON_SCALAR() expression. + */ +static Node * +transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *jsexpr) +{ + Node *arg = transformExprRecurse(pstate, (Node *) jsexpr->expr); + JsonOutput *output = jsexpr->output; + JsonReturning *returning; + + returning = transformJsonReturning(pstate, output, "JSON_SCALAR()"); + + if (exprType(arg) == UNKNOWNOID) + arg = coerce_to_specific_type(pstate, arg, TEXTOID, "JSON_SCALAR"); + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SCALAR, list_make1(arg), NULL, + returning, false, false, jsexpr->location); +} + +/* + * Transform a JSON_SERIALIZE() expression. + */ +static Node * +transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) +{ + Node *arg = transformJsonValueExpr(pstate, "JSON_SERIALIZE()", + expr->expr, + JS_FORMAT_JSON, + InvalidOid); + JsonReturning *returning; + + if (expr->output) + { + returning = transformJsonOutput(pstate, expr->output, true); + + if (returning->typid != BYTEAOID) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(returning->typid, &typcategory, + &typispreferred); + if (typcategory != TYPCATEGORY_STRING) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), + "JSON_SERIALIZE()"), + errhint("Try returning a string type or bytea."))); + } + } + else + { + /* RETURNING TEXT FORMAT JSON is by default */ + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(JS_FORMAT_JSON, JS_ENC_DEFAULT, -1); + returning->typid = TEXTOID; + returning->typmod = -1; + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), + NULL, returning, false, false, expr->location); +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4cca97ff9c..520d4f2a23 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1953,6 +1953,15 @@ FigureColnameInternal(Node *node, char **name) /* make XMLSERIALIZE act like a regular function */ *name = "xmlserialize"; return 2; + case T_JsonParseExpr: + *name = "json"; + return 2; + case T_JsonScalarExpr: + *name = "json_scalar"; + return 2; + case T_JsonSerializeExpr: + *name = "json_serialize"; + return 2; case T_JsonObjectConstructor: /* make JSON_OBJECT act like a regular function */ *name = "json_object"; diff --git a/src/backend/utils/adt/format_type.c b/src/backend/utils/adt/format_type.c index 12402a0637..36c45a3978 100644 --- a/src/backend/utils/adt/format_type.c +++ b/src/backend/utils/adt/format_type.c @@ -294,6 +294,10 @@ format_type_extended(Oid type_oid, int32 typemod, bits16 flags) else buf = pstrdup("character varying"); break; + + case JSONOID: + buf = pstrdup("json"); + break; } if (buf == NULL) diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index f6bef9c148..c316f848e1 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -653,6 +653,20 @@ row_to_json_pretty(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); } +/* + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. + */ +Datum +to_json_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid) +{ + StringInfo result = makeStringInfo(); + + datum_to_json(val, false, result, tcategory, outfuncoid, false); + + return PointerGetDatum(cstring_to_text_with_len(result->data, result->len)); +} + /* * Is the given type immutable when coming out of a JSON context? * @@ -704,7 +718,6 @@ to_json(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - StringInfo result; JsonTypeCategory tcategory; Oid outfuncoid; @@ -716,11 +729,7 @@ to_json(PG_FUNCTION_ARGS) json_categorize_type(val_type, false, &tcategory, &outfuncoid); - result = makeStringInfo(); - - datum_to_json(val, false, result, tcategory, outfuncoid, false); - - PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); + PG_RETURN_DATUM(to_json_worker(val, tcategory, outfuncoid)); } /* diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index fc64f56868..06ba409e64 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -33,6 +33,7 @@ typedef struct JsonbInState { JsonbParseState *parseState; JsonbValue *res; + bool unique_keys; Node *escontext; } JsonbInState; @@ -45,7 +46,8 @@ typedef struct JsonbAggState Oid val_output_func; } JsonbAggState; -static inline Datum jsonb_from_cstring(char *json, int len, Node *escontext); +static inline Datum jsonb_from_cstring(char *json, int len, bool unique_keys, + Node *escontext); static bool checkStringLen(size_t len, Node *escontext); static JsonParseErrorType jsonb_in_object_start(void *pstate); static JsonParseErrorType jsonb_in_object_end(void *pstate); @@ -76,7 +78,7 @@ jsonb_in(PG_FUNCTION_ARGS) { char *json = PG_GETARG_CSTRING(0); - return jsonb_from_cstring(json, strlen(json), fcinfo->context); + return jsonb_from_cstring(json, strlen(json), false, fcinfo->context); } /* @@ -100,7 +102,7 @@ jsonb_recv(PG_FUNCTION_ARGS) else elog(ERROR, "unsupported jsonb version number %d", version); - return jsonb_from_cstring(str, nbytes, NULL); + return jsonb_from_cstring(str, nbytes, false, NULL); } /* @@ -141,6 +143,18 @@ jsonb_send(PG_FUNCTION_ARGS) PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); } +/* + * Note: exported for use in SQL/JSON executor functions. + */ +Datum +jsonb_from_text(text *js, bool unique_keys) +{ + return jsonb_from_cstring(VARDATA_ANY(js), + VARSIZE_ANY_EXHDR(js), + unique_keys, + NULL); +} + /* * Get the type name of a jsonb container. */ @@ -234,7 +248,7 @@ jsonb_typeof(PG_FUNCTION_ARGS) * instead of being thrown. */ static inline Datum -jsonb_from_cstring(char *json, int len, Node *escontext) +jsonb_from_cstring(char *json, int len, bool unique_keys, Node *escontext) { JsonLexContext *lex; JsonbInState state; @@ -244,6 +258,7 @@ jsonb_from_cstring(char *json, int len, Node *escontext) memset(&sem, 0, sizeof(sem)); lex = makeJsonLexContextCstringLen(json, len, GetDatabaseEncoding(), true); + state.unique_keys = unique_keys; state.escontext = escontext; sem.semstate = (void *) &state; @@ -280,6 +295,7 @@ jsonb_in_object_start(void *pstate) JsonbInState *_state = (JsonbInState *) pstate; _state->res = pushJsonbValue(&_state->parseState, WJB_BEGIN_OBJECT, NULL); + _state->parseState->unique_keys = _state->unique_keys; return JSON_SUCCESS; } @@ -1021,6 +1037,23 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, datum_to_jsonb(val, is_null, result, tcategory, outfuncoid, key_scalar); } + +/* + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. + */ +Datum +to_jsonb_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid) +{ + JsonbInState result; + + memset(&result, 0, sizeof(JsonbInState)); + + datum_to_jsonb(val, false, &result, tcategory, outfuncoid, false); + + return JsonbPGetDatum(JsonbValueToJsonb(result.res)); +} + /* * Is the given type immutable when coming out of a JSONB context? * @@ -1072,7 +1105,6 @@ to_jsonb(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - JsonbInState result; JsonTypeCategory tcategory; Oid outfuncoid; @@ -1084,11 +1116,7 @@ to_jsonb(PG_FUNCTION_ARGS) json_categorize_type(val_type, true, &tcategory, &outfuncoid); - memset(&result, 0, sizeof(JsonbInState)); - - datum_to_jsonb(val, false, &result, tcategory, outfuncoid, false); - - PG_RETURN_POINTER(JsonbValueToJsonb(result.res)); + PG_RETURN_DATUM(to_jsonb_worker(val, tcategory, outfuncoid)); } Datum diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d3a973d86b..d1b03d6cb2 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -10832,6 +10832,15 @@ get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, case JSCTOR_JSON_ARRAY: funcname = "JSON_ARRAY"; break; + case JSCTOR_JSON_PARSE: + funcname = "JSON"; + break; + case JSCTOR_JSON_SCALAR: + funcname = "JSON_SCALAR"; + break; + case JSCTOR_JSON_SERIALIZE: + funcname = "JSON_SERIALIZE"; + break; default: elog(ERROR, "invalid JsonConstructorType %d", ctor->type); } @@ -10879,7 +10888,10 @@ get_json_constructor_options(JsonConstructorExpr *ctor, StringInfo buf) if (ctor->unique) appendStringInfoString(buf, " WITH UNIQUE KEYS"); - get_json_returning(ctor->returning, buf, true); + if (!((ctor->type == JSCTOR_JSON_PARSE || + ctor->type == JSCTOR_JSON_SCALAR) && + ctor->returning->typid == JSONOID)) + get_json_returning(ctor->returning, buf, true); } /* diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index efb5c3e098..d926713bd9 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1716,6 +1716,17 @@ typedef struct TriggerTransition /* Nodes for SQL/JSON support */ +/* + * JsonQuotes - + * representation of [KEEP|OMIT] QUOTES clause for JSON_QUERY() + */ +typedef enum JsonQuotes +{ + JS_QUOTES_UNSPEC, /* unspecified */ + JS_QUOTES_KEEP, /* KEEP QUOTES */ + JS_QUOTES_OMIT /* OMIT QUOTES */ +} JsonQuotes; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1739,6 +1750,43 @@ typedef struct JsonKeyValue JsonValueExpr *value; /* JSON value expression */ } JsonKeyValue; +/* + * JsonParseExpr - + * untransformed representation of JSON() + */ +typedef struct JsonParseExpr +{ + NodeTag type; + JsonValueExpr *expr; /* string expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + bool unique_keys; /* WITH UNIQUE KEYS? */ + int location; /* token location, or -1 if unknown */ +} JsonParseExpr; + +/* + * JsonScalarExpr - + * untransformed representation of JSON_SCALAR() + */ +typedef struct JsonScalarExpr +{ + NodeTag type; + Expr *expr; /* scalar expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonScalarExpr; + +/* + * JsonSerializeExpr - + * untransformed representation of JSON_SERIALIZE() function + */ +typedef struct JsonSerializeExpr +{ + NodeTag type; + JsonValueExpr *expr; /* json value expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonSerializeExpr; + /* * JsonObjectConstructor - * untransformed representation of JSON_OBJECT() constructor diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 0d2df069b3..85e484bf43 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1612,7 +1612,10 @@ typedef enum JsonConstructorType JSCTOR_JSON_OBJECT = 1, JSCTOR_JSON_ARRAY = 2, JSCTOR_JSON_OBJECTAGG = 3, - JSCTOR_JSON_ARRAYAGG = 4 + JSCTOR_JSON_ARRAYAGG = 4, + JSCTOR_JSON_SCALAR = 5, + JSCTOR_JSON_SERIALIZE = 6, + JSCTOR_JSON_PARSE = 7 } JsonConstructorType; /* diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index f5b2e61ca5..5984dcfa4b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -230,11 +230,13 @@ PG_KEYWORD("is", IS, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("isnull", ISNULL, TYPE_FUNC_NAME_KEYWORD, AS_LABEL) PG_KEYWORD("isolation", ISOLATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) -PG_KEYWORD("json", JSON, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index 649a1644f2..f928e6142a 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -368,7 +368,6 @@ typedef struct JsonbIterator struct JsonbIterator *parent; } JsonbIterator; - /* Convenience macros */ static inline Jsonb * DatumGetJsonbP(Datum d) @@ -418,6 +417,7 @@ extern void JsonbHashScalarValueExtended(const JsonbValue *scalarVal, uint64 *hash, uint64 seed); /* jsonb.c support functions */ +extern Datum jsonb_from_text(text *js, bool unique_keys); extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index 27a25ba283..7e9203b109 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -82,5 +82,9 @@ typedef enum void json_categorize_type(Oid typoid, bool is_jsonb, JsonTypeCategory *tcategory, Oid *outfuncoid); +extern Datum to_json_worker(Datum val, JsonTypeCategory tcategory, + Oid outfuncoid); +extern Datum to_jsonb_worker(Datum val, JsonTypeCategory tcategory, + Oid outfuncoid); #endif diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index d73c7e2c6c..d5074d73a2 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -1,3 +1,293 @@ +-- JSON() +SELECT JSON(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON(); + ^ +SELECT JSON(NULL); + json +------ + +(1 row) + +SELECT JSON('{ "a" : 1 } '); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +ERROR: JSON ENCODING clause is only allowed for bytea input type +LINE 1: SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); + ^ +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + pg_typeof +----------- + json +(1 row) + +SELECT JSON(' 1 '::json); + json +--------- + 1 +(1 row) + +SELECT JSON(' 1 '::jsonb); + json +------ + 1 +(1 row) + +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +ERROR: cannot use non-string types with WITH UNIQUE KEYS clause +LINE 1: SELECT JSON(' 1 '::json WITH UNIQUE KEYS); + ^ +SELECT JSON(123); +ERROR: cannot cast type integer to json +LINE 1: SELECT JSON(123); + ^ +SELECT JSON('{"a": 1, "a": 2}'); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +ERROR: duplicate JSON object key value +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); + QUERY PLAN +----------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); + QUERY PLAN +------------------------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON ENCODING UTF8) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); + QUERY PLAN +---------------------------------------------- + Result + Output: JSON('123'::text WITH UNIQUE KEYS) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +SELECT pg_typeof(JSON('123')); + pg_typeof +----------- + json +(1 row) + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SCALAR(); + ^ +SELECT JSON_SCALAR(NULL); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(NULL::int); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(123); + json_scalar +------------- + 123 +(1 row) + +SELECT JSON_SCALAR(123.45); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(123.45::numeric); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(true); + json_scalar +------------- + true +(1 row) + +SELECT JSON_SCALAR(false); + json_scalar +------------- + false +(1 row) + +SELECT JSON_SCALAR(' 123.45'); + json_scalar +------------- + " 123.45" +(1 row) + +SELECT JSON_SCALAR('2020-06-07'::date); + json_scalar +-------------- + "2020-06-07" +(1 row) + +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); + json_scalar +----------------------- + "2020-06-07T01:02:03" +(1 row) + +SELECT JSON_SCALAR('{}'::json); + json_scalar +------------- + {} +(1 row) + +SELECT JSON_SCALAR('{}'::jsonb); + json_scalar +------------- + {} +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); + QUERY PLAN +---------------------------- + Result + Output: JSON_SCALAR(123) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); + QUERY PLAN +------------------------------------ + Result + Output: JSON_SCALAR('123'::text) +(2 rows) + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SERIALIZE(); + ^ +SELECT JSON_SERIALIZE(NULL); + json_serialize +---------------- + +(1 row) + +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } '); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('1'); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('1' FORMAT JSON); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); + json_serialize +---------------------------- + \x7b20226122203a2031207d20 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + pg_typeof +----------- + text +(1 row) + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); +ERROR: cannot use RETURNING type jsonb in JSON_SERIALIZE() +HINT: Try returning a string type or bytea. +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); + QUERY PLAN +----------------------------------------------------- + Result + Output: JSON_SERIALIZE('{}'::json RETURNING text) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + QUERY PLAN +------------------------------------------------------ + Result + Output: JSON_SERIALIZE('{}'::json RETURNING bytea) +(2 rows) + -- JSON_OBJECT() SELECT JSON_OBJECT(); json_object @@ -630,6 +920,13 @@ ERROR: duplicate JSON object key SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); ERROR: duplicate JSON object key +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + json_objectagg +------------------ + {"1": 1, "2": 2} +(1 row) + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -645,6 +942,41 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); CREATE OR REPLACE VIEW public.json_object_view AS SELECT JSON_OBJECT('foo' : '1'::text FORMAT JSON, 'bar' : 'baz'::text RETURNING json) AS "json_object" DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + a | json_objectagg +---------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(2 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":1,"v":null} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1, "2" : 2 } + {"k":1,"v":null} | { "1" : 1, "2" : 2 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 4fd820fd51..e6e20175b0 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -1,3 +1,67 @@ +-- JSON() +SELECT JSON(); +SELECT JSON(NULL); +SELECT JSON('{ "a" : 1 } '); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + +SELECT JSON(' 1 '::json); +SELECT JSON(' 1 '::jsonb); +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +SELECT JSON(123); + +SELECT JSON('{"a": 1, "a": 2}'); +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +SELECT pg_typeof(JSON('123')); + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +SELECT JSON_SCALAR(NULL); +SELECT JSON_SCALAR(NULL::int); +SELECT JSON_SCALAR(123); +SELECT JSON_SCALAR(123.45); +SELECT JSON_SCALAR(123.45::numeric); +SELECT JSON_SCALAR(true); +SELECT JSON_SCALAR(false); +SELECT JSON_SCALAR(' 123.45'); +SELECT JSON_SCALAR('2020-06-07'::date); +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); +SELECT JSON_SCALAR('{}'::json); +SELECT JSON_SCALAR('{}'::jsonb); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +SELECT JSON_SERIALIZE(NULL); +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); +SELECT JSON_SERIALIZE('{ "a" : 1 } '); +SELECT JSON_SERIALIZE('1'); +SELECT JSON_SERIALIZE('1' FORMAT JSON); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); + + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + -- JSON_OBJECT() SELECT JSON_OBJECT(); SELECT JSON_OBJECT(RETURNING json); @@ -216,6 +280,9 @@ FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -227,6 +294,24 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index b10590a252..d251fb9a91 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1296,6 +1296,7 @@ JsonPathPredicateCallback JsonPathString JsonReturning JsonSemAction +JsonSerializeExpr JsonTokenType JsonTransformStringValuesAction JsonTypeCategory -- 2.35.3
v7-0004-JSON_TABLE.patch (application/octet-stream, 160.8K) download From 900d79cc0fcb9120f677ee9a9f18d39ae5d1aba9 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:14:12 +0900 Subject: [PATCH v7 4/5] JSON_TABLE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This feature allows jsonb data to be treated as a table and thus used in a FROM clause like other tabular data. Data can be selected from the jsonb using jsonpath expressions, and hoisted out of nested structures in the jsonb to form multiple rows, more or less like an outer join. This also adds the PLAN clauses for JSON_TABLE, which allow the user to specify how data from nested paths are joined, allowing considerable freedom in shaping the tabular output of JSON_TABLE. PLAN DEFAULT allows the user to specify the global strategies when dealing with sibling or child nested paths. The is often sufficient to achieve the necessary goal, and is considerably simpler than the full PLAN clause, which allows the user to specify the strategy to be used for each named nested path. Author: Nikita Glukhov <n.gluhov@postgrespro.ru> Author: Teodor Sigaev <teodor@sigaev.ru> Author: Oleg Bartunov <obartunov@gmail.com> Author: Alexander Korotkov <aekorotkov@gmail.com> Author: Andrew Dunstan <andrew@dunslane.net> Author: Amit Langote <amitlangote09@gmail.com> Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- doc/src/sgml/func.sgml | 448 ++++++++ src/backend/commands/explain.c | 8 +- src/backend/executor/execExprInterp.c | 5 + src/backend/executor/nodeTableFuncscan.c | 27 +- src/backend/nodes/makefuncs.c | 19 + src/backend/nodes/nodeFuncs.c | 30 + src/backend/parser/Makefile | 1 + src/backend/parser/gram.y | 476 ++++++++- src/backend/parser/meson.build | 1 + src/backend/parser/parse_clause.c | 14 +- src/backend/parser/parse_expr.c | 35 +- src/backend/parser/parse_jsontable.c | 739 +++++++++++++ src/backend/parser/parse_relation.c | 5 +- src/backend/parser/parse_target.c | 3 + src/backend/utils/adt/jsonpath_exec.c | 543 ++++++++++ src/backend/utils/adt/ruleutils.c | 279 ++++- src/include/nodes/execnodes.h | 2 + src/include/nodes/makefuncs.h | 2 + src/include/nodes/parsenodes.h | 90 ++ src/include/nodes/primnodes.h | 61 +- src/include/parser/kwlist.h | 4 + src/include/parser/parse_clause.h | 3 + src/include/utils/jsonpath.h | 3 + src/test/regress/expected/json_sqljson.out | 6 + src/test/regress/expected/jsonb_sqljson.out | 1069 +++++++++++++++++++ src/test/regress/sql/json_sqljson.sql | 4 + src/test/regress/sql/jsonb_sqljson.sql | 629 +++++++++++ src/tools/pgindent/typedefs.list | 13 + 28 files changed, 4486 insertions(+), 33 deletions(-) create mode 100644 src/backend/parser/parse_jsontable.c diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index cb36e1463b..44f016369e 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -17144,6 +17144,454 @@ array w/o UK? | t </sect2> + <sect2 id="functions-sqljson-table"> + <title>JSON_TABLE</title> + <indexterm> + <primary>json_table</primary> + </indexterm> + + <para> + <function>json_table</function> is an SQL/JSON function which + queries <acronym>JSON</acronym> data + and presents the results as a relational view, which can be accessed as a + regular SQL table. You can only use <function>json_table</function> inside the + <literal>FROM</literal> clause of a <literal>SELECT</literal> statement. + </para> + + <para> + Taking JSON data as input, <function>json_table</function> uses + a path expression to extract a part of the provided data that + will be used as a <firstterm>row pattern</firstterm> for the + constructed view. Each SQL/JSON item at the top level of the row pattern serves + as the source for a separate row in the constructed relational view. + </para> + + <para> + To split the row pattern into columns, <function>json_table</function> + provides the <literal>COLUMNS</literal> clause that defines the + schema of the created view. For each column to be constructed, + this clause provides a separate path expression that evaluates + the row pattern, extracts a JSON item, and returns it as a + separate SQL value for the specified column. If the required value + is stored in a nested level of the row pattern, it can be extracted + using the <literal>NESTED PATH</literal> subclause. Joining the + columns returned by <literal>NESTED PATH</literal> can add multiple + new rows to the constructed view. Such rows are called + <firstterm>child rows</firstterm>, as opposed to the <firstterm>parent row</firstterm> + that generates them. + </para> + + <para> + The rows produced by <function>JSON_TABLE</function> are laterally + joined to the row that generated them, so you do not have to explicitly join + the constructed view with the original table holding <acronym>JSON</acronym> + data. Optionally, you can specify how to join the columns returned + by <literal>NESTED PATH</literal> using the <literal>PLAN</literal> clause. + </para> + + <para> + Each <literal>NESTED PATH</literal> clause can generate one or more + columns. Columns produced by <literal>NESTED PATH</literal>s at the + same level are considered to be <firstterm>siblings</firstterm>, + while a column produced by a <literal>NESTED PATH</literal> is + considered to be a child of the column produced by a + <literal>NESTED PATH</literal> or row expression at a higher level. + Sibling columns are always joined first. Once they are processed, + the resulting rows are joined to the parent row. + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional></literal> + </term> + <listitem> + <para> + The input data to query, the JSON path expression defining the query, + and an optional <literal>PASSING</literal> clause, which can provide data + values to the <replaceable>path_expression</replaceable>. + The result of the input data + evaluation is called the <firstterm>row pattern</firstterm>. The row + pattern is used as the source for row values in the constructed view. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>COLUMNS</literal>( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + The <literal>COLUMNS</literal> clause defining the schema of the + constructed view. In this clause, you must specify all the columns + to be filled with SQL/JSON items. + The <replaceable>json_table_column</replaceable> + expression has the following syntax variants: + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>name</replaceable> <replaceable>type</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional></literal> + </term> + <listitem> + + <para> + Inserts a single SQL/JSON item into each row of + the specified column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable> + and fills the column with produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>ON EMPTY</literal> and + <literal>ON ERROR</literal> clauses to define how to handle missing values + or structural errors. + <literal>WRAPPER</literal> and <literal>QUOTES</literal> clauses can only + be used with JSON, array, and composite types. + These clauses have the same syntax and semantics as for + <function>json_value</function> and <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> <literal>FORMAT</literal> <replaceable>json_representation</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a composite SQL/JSON + item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable> + and fills the column with produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>WRAPPER</literal>, <literal>QUOTES</literal>, + <literal>ON EMPTY</literal> and <literal>ON ERROR</literal> clauses + to define additional settings for the returned SQL/JSON items. + These clauses have the same syntax and semantics as + for <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> + <literal>EXISTS</literal> <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a boolean item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression parses the + row pattern defined by <replaceable>json_api_common_syntax</replaceable>, + checks whether any SQL/JSON items were returned, and fills the column with + resulting boolean value, one for each row. + The specified <replaceable>type</replaceable> should have cast from + <type>boolean</type>. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + </para> + <para> + Optionally, you can add <literal>ON ERROR</literal> clause to define + error behavior. This clause has the same syntax and semantics as + for <function>json_exists</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>NESTED PATH</literal> <replaceable>json_path_specification</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> + <literal>COLUMNS</literal> ( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + Extracts SQL/JSON items from nested levels of the row pattern, + generates one or more columns as defined by the <literal>COLUMNS</literal> + subclause, and inserts the extracted SQL/JSON items into each row of these columns. + The <replaceable>json_table_column</replaceable> expression in the + <literal>COLUMNS</literal> subclause uses the same syntax as in the + parent <literal>COLUMNS</literal> clause. + </para> + + <para> + The <literal>NESTED PATH</literal> syntax is recursive, + so you can go down multiple nested levels by specifying several + <literal>NESTED PATH</literal> subclauses within each other. + It allows to unnest the hierarchy of JSON objects and arrays + in a single function invocation rather than chaining several + <function>JSON_TABLE</function> expressions in an SQL statement. + </para> + + <para> + You can use the <literal>PLAN</literal> clause to define how + to join the columns returned by <literal>NESTED PATH</literal> clauses. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <literal>FOR ORDINALITY</literal> + </term> + <listitem> + + <para> + Adds an ordinality column that provides sequential row numbering. + You can have only one ordinality column per table. Row numbering + is 1-based. For child rows that result from the <literal>NESTED PATH</literal> + clauses, the parent row number is repeated. + </para> + </listitem> + </varlistentry> + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>AS</literal> <replaceable>json_path_name</replaceable> + </term> + <listitem> + + <para> + The optional <replaceable>json_path_name</replaceable> serves as an + identifier of the provided <replaceable>json_path_specification</replaceable>. + The path name must be unique and distinct from the column names. + When using the <literal>PLAN</literal> clause, you must specify the names + for all the paths, including the row pattern. Each path name can appear in + the <literal>PLAN</literal> clause only once. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN</literal> ( <replaceable>json_table_plan</replaceable> ) + </term> + <listitem> + + <para> + Defines how to join the data returned by <literal>NESTED PATH</literal> + clauses to the constructed view. + </para> + <para> + To join columns with parent/child relationship, you can use: + </para> + <variablelist> + <varlistentry> + <term> + <literal>INNER</literal> + </term> + <listitem> + + <para> + Use <literal>INNER JOIN</literal>, so that the parent row + is omitted from the output if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>OUTER</literal> + </term> + <listitem> + + <para> + Use <literal>LEFT OUTER JOIN</literal>, so that the parent row + is always included into the output even if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>, with NULL values + inserted into the child columns if the corresponding + values are missing. + </para> + <para> + This is the default option for joining columns with parent/child relationship. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para> + To join sibling columns, you can use: + </para> + + <variablelist> + <varlistentry> + <term> + <literal>UNION</literal> + </term> + <listitem> + + <para> + Generate one row for each value produced by each of the sibling + columns. The columns from the other siblings are set to null. + </para> + <para> + This is the default option for joining sibling columns. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>CROSS</literal> + </term> + <listitem> + + <para> + Generate one row for each combination of values from the sibling columns. + </para> + </listitem> + </varlistentry> + + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN DEFAULT</literal> ( <literal><replaceable>OUTER | INNER</replaceable> <optional>, <replaceable>UNION | CROSS</replaceable> </optional></literal> ) + </term> + <listitem> + <para> + The terms can also be specified in reverse order. The + <literal>INNER</literal> or <literal>OUTER</literal> option defines the + joining plan for parent/child columns, while <literal>UNION</literal> or + <literal>CROSS</literal> affects joins of sibling columns. This form + of <literal>PLAN</literal> overrides the default plan for + all columns at once. Even though the path names are not included in the + <literal>PLAN DEFAULT</literal> form, to conform to the SQL/JSON standard + they must be provided for all the paths if the <literal>PLAN</literal> + clause is used. + </para> + <para> + <literal>PLAN DEFAULT</literal> is simpler than specifying a complete + <literal>PLAN</literal>, and is often all that is required to get the desired + output. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para>Examples</para> + + <para> + In these examples the following small table storing some JSON data will be used: +<programlisting> +CREATE TABLE my_films ( js jsonb ); + +INSERT INTO my_films VALUES ( +'{ "favorites" : [ + { "kind" : "comedy", "films" : [ + { "title" : "Bananas", + "director" : "Woody Allen"}, + { "title" : "The Dinner Game", + "director" : "Francis Veber" } ] }, + { "kind" : "horror", "films" : [ + { "title" : "Psycho", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "thriller", "films" : [ + { "title" : "Vertigo", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "drama", "films" : [ + { "title" : "Yojimbo", + "director" : "Akira Kurosawa" } ] } + ] }'); +</programlisting> + </para> + <para> + Query the <structname>my_films</structname> table holding + some JSON data about the films and create a view that + distributes the film genre, title, and director between separate columns: +<screen> +SELECT jt.* FROM + my_films, + JSON_TABLE ( js, '$.favorites[*]' COLUMNS ( + id FOR ORDINALITY, + kind text PATH '$.kind', + NESTED PATH '$.films[*]' COLUMNS ( + title text PATH '$.title', + director text PATH '$.director'))) AS jt; +----+----------+------------------+------------------- + id | kind | title | director +----+----------+------------------+------------------- + 1 | comedy | Bananas | Woody Allen + 1 | comedy | The Dinner Game | Francis Veber + 2 | horror | Psycho | Alfred Hitchcock + 3 | thriller | Vertigo | Alfred Hitchcock + 4 | drama | Yojimbo | Akira Kurosawa + (5 rows) +</screen> + </para> + + <para> + Find a director that has done films in two different genres: +<screen> +SELECT + director1 AS director, title1, kind1, title2, kind2 +FROM + my_films, + JSON_TABLE ( js, '$.favorites' AS favs COLUMNS ( + NESTED PATH '$[*]' AS films1 COLUMNS ( + kind1 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film1 COLUMNS ( + title1 text PATH '$.title', + director1 text PATH '$.director') + ), + NESTED PATH '$[*]' AS films2 COLUMNS ( + kind2 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film2 COLUMNS ( + title2 text PATH '$.title', + director2 text PATH '$.director' + ) + ) + ) + PLAN (favs OUTER ((films1 INNER film1) CROSS (films2 INNER film2))) + ) AS jt + WHERE kind1 > kind2 AND director1 = director2; + + director | title1 | kind1 | title2 | kind2 +------------------+---------+----------+--------+-------- + Alfred Hitchcock | Vertigo | thriller | Psycho | horror +(1 row) +</screen> + </para> + </sect2> + <sect2 id="functions-sqljson-path"> <title>The SQL/JSON Path Language</title> diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 8570b14f62..ad899de5d6 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -3862,7 +3862,13 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es) break; case T_TableFuncScan: Assert(rte->rtekind == RTE_TABLEFUNC); - objectname = "xmltable"; + if (rte->tablefunc) + if (rte->tablefunc->functype == TFT_XMLTABLE) + objectname = "xmltable"; + else /* Must be TFT_JSON_TABLE */ + objectname = "json_table"; + else + objectname = NULL; objecttag = "Table Function Name"; break; case T_ValuesScan: diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 4c97e714ea..84c8730129 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -4309,6 +4309,11 @@ ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) break; } + case JSON_TABLE_OP: + res = item; + resnull = false; + break; + default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); *op->resnull = true; diff --git a/src/backend/executor/nodeTableFuncscan.c b/src/backend/executor/nodeTableFuncscan.c index 791cbd2372..085a612533 100644 --- a/src/backend/executor/nodeTableFuncscan.c +++ b/src/backend/executor/nodeTableFuncscan.c @@ -28,6 +28,7 @@ #include "miscadmin.h" #include "nodes/execnodes.h" #include "utils/builtins.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/xml.h" @@ -161,8 +162,9 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) scanstate->ss.ps.qual = ExecInitQual(node->scan.plan.qual, &scanstate->ss.ps); - /* Only XMLTABLE is supported currently */ - scanstate->routine = &XmlTableRoutine; + /* Only XMLTABLE and JSON_TABLE are supported currently */ + scanstate->routine = + tf->functype == TFT_XMLTABLE ? &XmlTableRoutine : &JsonbTableRoutine; scanstate->perTableCxt = AllocSetContextCreate(CurrentMemoryContext, @@ -182,6 +184,10 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) ExecInitExprList(tf->colexprs, (PlanState *) scanstate); scanstate->coldefexprs = ExecInitExprList(tf->coldefexprs, (PlanState *) scanstate); + scanstate->colvalexprs = + ExecInitExprList(tf->colvalexprs, (PlanState *) scanstate); + scanstate->passingvalexprs = + ExecInitExprList(tf->passingvalexprs, (PlanState *) scanstate); scanstate->notnulls = tf->notnulls; @@ -381,14 +387,17 @@ tfuncInitialize(TableFuncScanState *tstate, ExprContext *econtext, Datum doc) routine->SetNamespace(tstate, ns_name, ns_uri); } - /* Install the row filter expression into the table builder context */ - value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); - if (isnull) - ereport(ERROR, - (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), - errmsg("row filter expression must not be null"))); + if (routine->SetRowFilter) + { + /* Install the row filter expression into the table builder context */ + value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); + if (isnull) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("row filter expression must not be null"))); - routine->SetRowFilter(tstate, TextDatumGetCString(value)); + routine->SetRowFilter(tstate, TextDatumGetCString(value)); + } /* * Install the column filter expressions into the table builder context. diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 7bcc12b04f..7a03283713 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -878,6 +878,25 @@ makeJsonBehavior(JsonBehaviorType type, Node *default_expr) return behavior; } +/* + * makeJsonTableJoinedPlan - + * creates a joined JsonTablePlan node + */ +Node * +makeJsonTableJoinedPlan(JsonTablePlanJoinType type, Node *plan1, Node *plan2, + int location) +{ + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_JOINED; + n->join_type = type; + n->plan1 = castNode(JsonTablePlan, plan1); + n->plan2 = castNode(JsonTablePlan, plan2); + n->location = location; + + return (Node *) n; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index d31371bb4d..e07c066e15 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -2631,6 +2631,10 @@ expression_tree_walker_impl(Node *node, return true; if (WALK(tf->coldefexprs)) return true; + if (WALK(tf->colvalexprs)) + return true; + if (WALK(tf->passingvalexprs)) + return true; } break; default: @@ -3699,6 +3703,8 @@ expression_tree_mutator_impl(Node *node, MUTATE(newnode->rowexpr, tf->rowexpr, Node *); MUTATE(newnode->colexprs, tf->colexprs, List *); MUTATE(newnode->coldefexprs, tf->coldefexprs, List *); + MUTATE(newnode->colvalexprs, tf->colvalexprs, List *); + MUTATE(newnode->passingvalexprs, tf->passingvalexprs, List *); return (Node *) newnode; } break; @@ -4130,6 +4136,30 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonTable: + { + JsonTable *jt = (JsonTable *) node; + + if (WALK(jt->common)) + return true; + if (WALK(jt->columns)) + return true; + } + break; + case T_JsonTableColumn: + { + JsonTableColumn *jtc = (JsonTableColumn *) node; + + if (WALK(jtc->typeName)) + return true; + if (WALK(jtc->on_empty)) + return true; + if (WALK(jtc->on_error)) + return true; + if (jtc->coltype == JTC_NESTED && WALK(jtc->columns)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile index 9f1c4022bb..f4c0cc7f10 100644 --- a/src/backend/parser/Makefile +++ b/src/backend/parser/Makefile @@ -23,6 +23,7 @@ OBJS = \ parse_enr.o \ parse_expr.o \ parse_func.o \ + parse_jsontable.o \ parse_merge.o \ parse_node.o \ parse_oper.o \ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 0de553f5f9..6a70732b6f 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -654,19 +654,43 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_aggregate_func json_api_common_syntax json_argument + json_table + json_table_column_definition + json_table_ordinality_column_definition + json_table_regular_column_definition + json_table_formatted_column_definition + json_table_exists_column_definition + json_table_nested_columns + json_table_plan_clause_opt + json_table_plan + json_table_plan_simple + json_table_plan_parent_child + json_table_plan_outer + json_table_plan_inner + json_table_plan_sibling + json_table_plan_union + json_table_plan_cross + json_table_plan_primary %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt json_arguments + json_table_columns_clause + json_table_column_definition_list +%type <str> json_table_column_path_specification_clause_opt %type <ival> json_encoding_clause_opt json_predicate_type_constraint json_wrapper_behavior + json_table_default_plan_choices + json_table_default_plan_inner_outer + json_table_default_plan_union_cross %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt %type <jsbehavior> json_value_behavior json_query_behavior json_exists_behavior + json_table_behavior %type <js_quotes> json_quotes_clause_opt /* @@ -732,7 +756,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG - JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_TABLE JSON_VALUE KEY KEYS KEEP @@ -743,8 +767,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE - NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE - NORMALIZE NORMALIZED + NAME_P NAMES NATIONAL NATURAL NCHAR NESTED NEW NEXT NFC NFD NFKC NFKD NO + NONE NORMALIZE NORMALIZED NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC @@ -752,8 +776,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER - PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD - PLACING PLANS POLICY + PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH + PLACING PLAN PLANS POLICY POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION @@ -861,6 +885,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); * Using the same precedence as IDENT seems right for the reasons given above. */ %nonassoc UNBOUNDED /* ideally would have same precedence as IDENT */ +%nonassoc COLUMNS %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' @@ -883,6 +908,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL +%nonassoc json_table_column +%nonassoc NESTED +%left PATH %% /* @@ -13324,6 +13352,21 @@ table_ref: relation_expr opt_alias_clause $2->alias = $4; $$ = (Node *) $2; } + | json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $1); + + jt->alias = $2; + $$ = (Node *) jt; + } + | LATERAL_P json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $2); + + jt->alias = $3; + jt->lateral = true; + $$ = (Node *) jt; + } ; @@ -13891,6 +13934,8 @@ xmltable_column_option_el: { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(true), @1); } | NULL_P { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(false), @1); } + | PATH b_expr + { $$ = makeDefElem("path", $2, @1); } ; xml_namespace_list: @@ -16696,6 +16741,11 @@ json_value_behavior: | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } ; +json_table_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + ; + /* ARRAY is a noise word */ json_wrapper_behavior: WITHOUT WRAPPER { $$ = JSW_NONE; } @@ -16717,6 +16767,414 @@ json_quotes_clause_opt: | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } ; +json_table: + JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + json_table_behavior ON ERROR_P + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_columns_clause: + COLUMNS '(' json_table_column_definition_list ')' { $$ = $3; } + ; + +json_table_column_definition_list: + json_table_column_definition + { $$ = list_make1($1); } + | json_table_column_definition_list ',' json_table_column_definition + { $$ = lappend($1, $3); } + ; + +json_table_column_definition: + json_table_ordinality_column_definition %prec json_table_column + | json_table_regular_column_definition %prec json_table_column + | json_table_formatted_column_definition %prec json_table_column + | json_table_exists_column_definition %prec json_table_column + | json_table_nested_columns + ; + +json_table_ordinality_column_definition: + ColId FOR ORDINALITY + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FOR_ORDINALITY; + n->name = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_regular_column_definition: + ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_exists_column_definition: + ColId Typename + EXISTS json_table_column_path_specification_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + EXISTS json_table_column_path_specification_clause_opt + json_exists_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_column_path_specification_clause_opt: + PATH Sconst { $$ = $2; } + | /* EMPTY */ %prec json_table_column { $$ = NULL; } + ; + +json_table_formatted_column_definition: + ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->on_error = $12; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_nested_columns: + NESTED path_opt Sconst + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = NULL; + n->columns = $4; + n->location = @1; + $$ = (Node *) n; + } + | NESTED path_opt Sconst AS name + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = $5; + n->columns = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +path_opt: + PATH { } + | /* EMPTY */ { } + ; + +json_table_plan_clause_opt: + PLAN '(' json_table_plan ')' { $$ = $3; } + | PLAN DEFAULT '(' json_table_default_plan_choices ')' + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_DEFAULT; + n->join_type = $4; + n->location = @1; + $$ = (Node *) n; + } + | /* EMPTY */ { $$ = NULL; } + ; + +json_table_plan: + json_table_plan_simple + | json_table_plan_parent_child + | json_table_plan_sibling + ; + +json_table_plan_simple: + name + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_SIMPLE; + n->pathname = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_plan_primary: + json_table_plan_simple { $$ = $1; } + | '(' json_table_plan ')' + { + castNode(JsonTablePlan, $2)->location = @1; + $$ = $2; + } + ; + +json_table_plan_outer: + json_table_plan_simple OUTER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_OUTER, $1, $3, @1); } + ; + +json_table_plan_inner: + json_table_plan_simple INNER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_INNER, $1, $3, @1); } + ; + +json_table_plan_parent_child: + json_table_plan_outer + | json_table_plan_inner + ; + +json_table_plan_union: + json_table_plan_primary UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + | json_table_plan_union UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + ; + +json_table_plan_cross: + json_table_plan_primary CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + | json_table_plan_cross CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + ; + +json_table_plan_sibling: + json_table_plan_union + | json_table_plan_cross + ; + +json_table_default_plan_choices: + json_table_default_plan_inner_outer { $$ = $1 | JSTPJ_UNION; } + | json_table_default_plan_union_cross { $$ = $1 | JSTPJ_OUTER; } + | json_table_default_plan_inner_outer ',' + json_table_default_plan_union_cross { $$ = $1 | $3; } + | json_table_default_plan_union_cross ',' + json_table_default_plan_inner_outer { $$ = $1 | $3; } + ; + +json_table_default_plan_inner_outer: + INNER_P { $$ = JSTPJ_INNER; } + | OUTER_P { $$ = JSTPJ_OUTER; } + ; + +json_table_default_plan_union_cross: + UNION { $$ = JSTPJ_UNION; } + | CROSS { $$ = JSTPJ_CROSS; } + ; + json_returning_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17441,6 +17899,7 @@ unreserved_keyword: | MOVE | NAME_P | NAMES + | NESTED | NEW | NEXT | NFC @@ -17475,6 +17934,8 @@ unreserved_keyword: | PARTITION | PASSING | PASSWORD + | PATH + | PLAN | PLANS | POLICY | PRECEDING @@ -17639,6 +18100,7 @@ col_name_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | LEAST | NATIONAL @@ -18007,6 +18469,7 @@ bare_label_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | KEEP | KEY @@ -18046,6 +18509,7 @@ bare_label_keyword: | NATIONAL | NATURAL | NCHAR + | NESTED | NEW | NEXT | NFC @@ -18090,7 +18554,9 @@ bare_label_keyword: | PARTITION | PASSING | PASSWORD + | PATH | PLACING + | PLAN | PLANS | POLICY | POSITION diff --git a/src/backend/parser/meson.build b/src/backend/parser/meson.build index 94517a7f7d..0f0e4ac7ca 100644 --- a/src/backend/parser/meson.build +++ b/src/backend/parser/meson.build @@ -10,6 +10,7 @@ backend_sources += files( 'parse_enr.c', 'parse_expr.c', 'parse_func.c', + 'parse_jsontable.c', 'parse_merge.c', 'parse_node.c', 'parse_oper.c', diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index 334b9b42bd..affd812619 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -697,7 +697,11 @@ transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf) char **names; int colno; - /* Currently only XMLTABLE is supported */ + /* + * Currently we only support XMLTABLE here. See transformJsonTable() for + * JSON_TABLE support. + */ + tf->functype = TFT_XMLTABLE; constructName = "XMLTABLE"; docType = XMLOID; @@ -1104,13 +1108,17 @@ transformFromClauseItem(ParseState *pstate, Node *n, rtr->rtindex = nsitem->p_rtindex; return (Node *) rtr; } - else if (IsA(n, RangeTableFunc)) + else if (IsA(n, RangeTableFunc) || IsA(n, JsonTable)) { /* table function is like a plain relation */ RangeTblRef *rtr; ParseNamespaceItem *nsitem; - nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + if (IsA(n, RangeTableFunc)) + nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + else + nsitem = transformJsonTable(pstate, (JsonTable *) n); + *top_nsitem = nsitem; *namespace = list_make1(nsitem); rtr = makeNode(RangeTblRef); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 89343e0d6a..1c54dca905 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4227,7 +4227,7 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) JsonFormatType format; char *constructName; - if (func->common->pathname) + if (func->common->pathname && func->op != JSON_TABLE_OP) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("JSON_TABLE path name is not allowed here"), @@ -4246,6 +4246,9 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) case JSON_EXISTS_OP: constructName = "JSON_EXISTS()"; break; + case JSON_TABLE_OP: + constructName = "JSON_TABLE()"; + break; default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); break; @@ -4285,14 +4288,19 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) &jsexpr->passing_values, &jsexpr->passing_names); - if (func->op != JSON_EXISTS_OP) + if (func->op != JSON_EXISTS_OP && func->op != JSON_TABLE_OP) jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, JSON_BEHAVIOR_NULL); - jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, - func->op == JSON_EXISTS_OP ? - JSON_BEHAVIOR_FALSE : - JSON_BEHAVIOR_NULL); + if (func->op == JSON_EXISTS_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_FALSE); + else if (func->op == JSON_TABLE_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_EMPTY); + else + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_NULL); return jsexpr; } @@ -4616,6 +4624,21 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->result_coercion->expr = NULL; } break; + + case JSON_TABLE_OP: + jsexpr->returning = makeNode(JsonReturning); + jsexpr->returning->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + jsexpr->returning->typid = exprType(contextItemExpr); + jsexpr->returning->typmod = -1; + + if (jsexpr->returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("JSON_TABLE() is not yet implemented for the json type"), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + break; } if (exprType(contextItemExpr) != JSONBOID) diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c new file mode 100644 index 0000000000..a7802e0499 --- /dev/null +++ b/src/backend/parser/parse_jsontable.c @@ -0,0 +1,739 @@ +/*------------------------------------------------------------------------- + * + * parse_jsontable.c + * parsing of JSON_TABLE + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/parser/parse_jsontable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/pg_collation.h" +#include "catalog/pg_type.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "optimizer/optimizer.h" +#include "parser/parse_clause.h" +#include "parser/parse_collate.h" +#include "parser/parse_expr.h" +#include "parser/parse_relation.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/json.h" +#include "utils/lsyscache.h" + +/* Context for JSON_TABLE transformation */ +typedef struct JsonTableParseContext +{ + ParseState *pstate; /* parsing state */ + JsonTable *table; /* untransformed node */ + TableFunc *tablefunc; /* transformed node */ + List *pathNames; /* list of all path and columns names */ + int pathNameId; /* path name id counter */ + Oid contextItemTypid; /* type oid of context item (json/jsonb) */ +} JsonTableParseContext; + +static JsonTableParent *transformJsonTableColumns(JsonTableParseContext *cxt, + JsonTablePlan *plan, + List *columns, + char *pathSpec, + char **pathName, + int location); + +static Node * +makeStringConst(char *str, int location) +{ + A_Const *n = makeNode(A_Const); + + n->val.node.type = T_String; + n->val.sval.sval = str; + n->location = location; + + return (Node *) n; +} + +/* + * Transform JSON_TABLE column + * - regular column into JSON_VALUE() + * - FORMAT JSON column into JSON_QUERY() + * - EXISTS column into JSON_EXISTS() + */ +static Node * +transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, + List *passingArgs, bool errorOnError) +{ + JsonFuncExpr *jfexpr = makeNode(JsonFuncExpr); + JsonCommon *common = makeNode(JsonCommon); + JsonOutput *output = makeNode(JsonOutput); + char *pathspec; + JsonFormat *default_format; + + jfexpr->op = + jtc->coltype == JTC_REGULAR ? JSON_VALUE_OP : + jtc->coltype == JTC_EXISTS ? JSON_EXISTS_OP : JSON_QUERY_OP; + jfexpr->common = common; + jfexpr->output = output; + jfexpr->on_empty = jtc->on_empty; + jfexpr->on_error = jtc->on_error; + if (!jfexpr->on_error && errorOnError) + jfexpr->on_error = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); + jfexpr->omit_quotes = jtc->omit_quotes; + jfexpr->wrapper = jtc->wrapper; + jfexpr->location = jtc->location; + + output->typeName = jtc->typeName; + output->returning = makeNode(JsonReturning); + output->returning->format = jtc->format; + + default_format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + + common->pathname = NULL; + common->expr = makeJsonValueExpr((Expr *) contextItemExpr, default_format); + common->passing = passingArgs; + + if (jtc->pathspec) + pathspec = jtc->pathspec; + else + { + /* Construct default path as '$."column_name"' */ + StringInfoData path; + + initStringInfo(&path); + + appendStringInfoString(&path, "$."); + escape_json(&path, jtc->name); + + pathspec = path.data; + } + + common->pathspec = makeStringConst(pathspec, -1); + + return (Node *) jfexpr; +} + +static bool +isJsonTablePathNameDuplicate(JsonTableParseContext *cxt, const char *pathname) +{ + ListCell *lc; + + foreach(lc, cxt->pathNames) + { + if (!strcmp(pathname, (const char *) lfirst(lc))) + return true; + } + + return false; +} + +/* Register the column name in the path name list. */ +static void +registerJsonTableColumn(JsonTableParseContext *cxt, char *colname) +{ + if (isJsonTablePathNameDuplicate(cxt, colname)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_ALIAS), + errmsg("duplicate JSON_TABLE column name: %s", colname), + errhint("JSON_TABLE column names must be distinct from one another."))); + + cxt->pathNames = lappend(cxt->pathNames, colname); +} + +/* Recursively register all nested column names in the path name list. */ +static void +registerAllJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED) + { + if (jtc->pathname) + registerJsonTableColumn(cxt, jtc->pathname); + + registerAllJsonTableColumns(cxt, jtc->columns); + } + else + { + registerJsonTableColumn(cxt, jtc->name); + } + } +} + +/* Generate a new unique JSON_TABLE path name. */ +static char * +generateJsonTablePathName(JsonTableParseContext *cxt) +{ + char namebuf[32]; + char *name = namebuf; + + do + { + snprintf(namebuf, sizeof(namebuf), "json_table_path_%d", + ++cxt->pathNameId); + } while (isJsonTablePathNameDuplicate(cxt, name)); + + name = pstrdup(name); + cxt->pathNames = lappend(cxt->pathNames, name); + + return name; +} + +/* Collect sibling path names from plan to the specified list. */ +static void +collectSiblingPathsInJsonTablePlan(JsonTablePlan *plan, List **paths) +{ + if (plan->plan_type == JSTP_SIMPLE) + *paths = lappend(*paths, plan->pathname); + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + *paths = lappend(*paths, plan->plan1->pathname); + } + else if (plan->join_type == JSTPJ_CROSS || + plan->join_type == JSTPJ_UNION) + { + collectSiblingPathsInJsonTablePlan(plan->plan1, paths); + collectSiblingPathsInJsonTablePlan(plan->plan2, paths); + } + else + elog(ERROR, "invalid JSON_TABLE join type %d", + plan->join_type); + } +} + +/* + * Validate child JSON_TABLE plan by checking that: + * - all nested columns have path names specified + * - all nested columns have corresponding node in the sibling plan + * - plan does not contain duplicate or extra nodes + */ +static void +validateJsonTableChildPlan(ParseState *pstate, JsonTablePlan *plan, + List *columns) +{ + ListCell *lc1; + List *siblings = NIL; + int nchildren = 0; + + if (plan) + collectSiblingPathsInJsonTablePlan(plan, &siblings); + + foreach(lc1, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc1)); + + if (jtc->coltype == JTC_NESTED) + { + ListCell *lc2; + bool found = false; + + if (!jtc->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("nested JSON_TABLE columns must contain an explicit AS pathname specification if an explicit PLAN clause is used"), + parser_errposition(pstate, jtc->location))); + + /* find nested path name in the list of sibling path names */ + foreach(lc2, siblings) + { + if ((found = !strcmp(jtc->pathname, lfirst(lc2)))) + break; + } + + if (!found) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node for nested path %s was not found in plan.", jtc->pathname), + parser_errposition(pstate, jtc->location))); + + nchildren++; + } + } + + if (list_length(siblings) > nchildren) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node contains some extra or duplicate sibling nodes."), + parser_errposition(pstate, plan ? plan->location : -1))); +} + +static JsonTableColumn * +findNestedJsonTableColumn(List *columns, const char *pathname) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED && + jtc->pathname && + !strcmp(jtc->pathname, pathname)) + return jtc; + } + + return NULL; +} + +static Node * +transformNestedJsonTableColumn(JsonTableParseContext *cxt, JsonTableColumn *jtc, + JsonTablePlan *plan) +{ + JsonTableParent *node; + char *pathname = jtc->pathname; + + node = transformJsonTableColumns(cxt, plan, jtc->columns, jtc->pathspec, + &pathname, jtc->location); + + return (Node *) node; +} + +static Node * +makeJsonTableSiblingJoin(bool cross, Node *lnode, Node *rnode) +{ + JsonTableSibling *join = makeNode(JsonTableSibling); + + join->larg = lnode; + join->rarg = rnode; + join->cross = cross; + + return (Node *) join; +} + +/* + * Recursively transform child JSON_TABLE plan. + * + * Default plan is transformed into a cross/union join of its nested columns. + * Simple and outer/inner plans are transformed into a JsonTableParent by + * finding and transforming corresponding nested column. + * Sibling plans are recursively transformed into a JsonTableSibling. + */ +static Node * +transformJsonTableChildPlan(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns) +{ + JsonTableColumn *jtc = NULL; + + if (!plan || plan->plan_type == JSTP_DEFAULT) + { + /* unspecified or default plan */ + Node *res = NULL; + ListCell *lc; + bool cross = plan && (plan->join_type & JSTPJ_CROSS); + + /* transform all nested columns into cross/union join */ + foreach(lc, columns) + { + JsonTableColumn *col = castNode(JsonTableColumn, lfirst(lc)); + Node *node; + + if (col->coltype != JTC_NESTED) + continue; + + node = transformNestedJsonTableColumn(cxt, col, plan); + + /* join transformed node with previous sibling nodes */ + res = res ? makeJsonTableSiblingJoin(cross, res, node) : node; + } + + return res; + } + else if (plan->plan_type == JSTP_SIMPLE) + { + jtc = findNestedJsonTableColumn(columns, plan->pathname); + } + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + jtc = findNestedJsonTableColumn(columns, plan->plan1->pathname); + } + else + { + Node *node1 = transformJsonTableChildPlan(cxt, plan->plan1, + columns); + Node *node2 = transformJsonTableChildPlan(cxt, plan->plan2, + columns); + + return makeJsonTableSiblingJoin(plan->join_type == JSTPJ_CROSS, + node1, node2); + } + } + else + elog(ERROR, "invalid JSON_TABLE plan type %d", plan->plan_type); + + if (!jtc) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name was %s not found in nested columns list.", + plan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + return transformNestedJsonTableColumn(cxt, jtc, plan); +} + +/* Check whether type is json/jsonb, array, or record. */ +static bool +typeIsComposite(Oid typid) +{ + char typtype; + + if (typid == JSONOID || + typid == JSONBOID || + typid == RECORDOID || + type_is_array(typid)) + return true; + + typtype = get_typtype(typid); + + if (typtype == TYPTYPE_COMPOSITE) + return true; + + if (typtype == TYPTYPE_DOMAIN) + return typeIsComposite(getBaseType(typid)); + + return false; +} + +/* Append transformed non-nested JSON_TABLE columns to the TableFunc node */ +static void +appendJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *col; + ParseState *pstate = cxt->pstate; + JsonTable *jt = cxt->table; + TableFunc *tf = cxt->tablefunc; + bool errorOnError = jt->on_error && + jt->on_error->btype == JSON_BEHAVIOR_ERROR; + + foreach(col, columns) + { + JsonTableColumn *rawc = castNode(JsonTableColumn, lfirst(col)); + Oid typid; + int32 typmod; + Node *colexpr; + + if (rawc->name) + { + /* make sure column names are unique */ + ListCell *colname; + + foreach(colname, tf->colnames) + if (!strcmp((const char *) colname, rawc->name)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("column name \"%s\" is not unique", + rawc->name), + parser_errposition(pstate, rawc->location))); + + tf->colnames = lappend(tf->colnames, + makeString(pstrdup(rawc->name))); + } + + /* + * Determine the type and typmod for the new column. FOR ORDINALITY + * columns are INTEGER by standard; the others are user-specified. + */ + switch (rawc->coltype) + { + case JTC_FOR_ORDINALITY: + colexpr = NULL; + typid = INT4OID; + typmod = -1; + break; + + case JTC_REGULAR: + typenameTypeIdAndMod(pstate, rawc->typeName, &typid, &typmod); + + /* + * Use implicit FORMAT JSON for composite types (arrays and + * records) + */ + if (typeIsComposite(typid)) + rawc->coltype = JTC_FORMATTED; + else if (rawc->wrapper != JSW_NONE) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use WITH WRAPPER clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + else if (rawc->omit_quotes) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use OMIT QUOTES clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + + /* FALLTHROUGH */ + case JTC_EXISTS: + case JTC_FORMATTED: + { + Node *je; + CaseTestExpr *param = makeNode(CaseTestExpr); + + param->collation = InvalidOid; + param->typeId = cxt->contextItemTypid; + param->typeMod = -1; + + je = transformJsonTableColumn(rawc, (Node *) param, + NIL, errorOnError); + + colexpr = transformExpr(pstate, je, EXPR_KIND_FROM_FUNCTION); + assign_expr_collations(pstate, colexpr); + + typid = exprType(colexpr); + typmod = exprTypmod(colexpr); + break; + } + + case JTC_NESTED: + continue; + + default: + elog(ERROR, "unknown JSON_TABLE column type: %d", rawc->coltype); + break; + } + + tf->coltypes = lappend_oid(tf->coltypes, typid); + tf->coltypmods = lappend_int(tf->coltypmods, typmod); + tf->colcollations = lappend_oid(tf->colcollations, get_typcollation(typid)); + tf->colvalexprs = lappend(tf->colvalexprs, colexpr); + } +} + +/* + * Create transformed JSON_TABLE parent plan node by appending all non-nested + * columns to the TableFunc node and remembering their indices in the + * colvalexprs list. + */ +static JsonTableParent * +makeParentJsonTableNode(JsonTableParseContext *cxt, char *pathSpec, char *pathName, + List *columns) +{ + JsonTableParent *node = makeNode(JsonTableParent); + + node->path = makeNode(JsonTablePath); + node->path->value = makeConst(JSONPATHOID, -1, InvalidOid, -1, + DirectFunctionCall1(jsonpath_in, + CStringGetDatum(pathSpec)), + false, false); + if (pathName) + node->path->name = pstrdup(pathName); + + /* save start of column range */ + node->colMin = list_length(cxt->tablefunc->colvalexprs); + + appendJsonTableColumns(cxt, columns); + + /* save end of column range */ + node->colMax = list_length(cxt->tablefunc->colvalexprs) - 1; + + node->errorOnError = + cxt->table->on_error && + cxt->table->on_error->btype == JSON_BEHAVIOR_ERROR; + + return node; +} + +static JsonTableParent * +transformJsonTableColumns(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns, char *pathSpec, char **pathName, + int location) +{ + JsonTableParent *node; + JsonTablePlan *childPlan; + bool defaultPlan = !plan || plan->plan_type == JSTP_DEFAULT; + + if (!*pathName) + { + if (cxt->table->plan) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE expression"), + errdetail("JSON_TABLE columns must contain " + "explicit AS pathname specification if " + "explicit PLAN clause is used"), + parser_errposition(cxt->pstate, location))); + + *pathName = generateJsonTablePathName(cxt); + } + + if (defaultPlan) + childPlan = plan; + else + { + /* validate parent and child plans */ + JsonTablePlan *parentPlan; + + if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type != JSTPJ_INNER && + plan->join_type != JSTPJ_OUTER) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Expected INNER or OUTER JSON_TABLE plan node."), + parser_errposition(cxt->pstate, plan->location))); + + parentPlan = plan->plan1; + childPlan = plan->plan2; + + Assert(parentPlan->plan_type != JSTP_JOINED); + Assert(parentPlan->pathname); + } + else + { + parentPlan = plan; + childPlan = NULL; + } + + if (strcmp(parentPlan->pathname, *pathName)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name mismatch: expected %s but %s is given.", + *pathName, parentPlan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + validateJsonTableChildPlan(cxt->pstate, childPlan, columns); + } + + /* transform only non-nested columns */ + node = makeParentJsonTableNode(cxt, pathSpec, *pathName, columns); + + if (childPlan || defaultPlan) + { + /* transform recursively nested columns */ + node->child = transformJsonTableChildPlan(cxt, childPlan, columns); + if (node->child) + node->outerJoin = !plan || (plan->join_type & JSTPJ_OUTER); + /* else: default plan case, no children found */ + } + + return node; +} + +/* + * transformJsonTable - + * Transform a raw JsonTable into TableFunc. + * + * Transform the document-generating expression, the row-generating expression, + * the column-generating expressions, and the default value expressions. + */ +ParseNamespaceItem * +transformJsonTable(ParseState *pstate, JsonTable *jt) +{ + JsonTableParseContext cxt; + TableFunc *tf = makeNode(TableFunc); + JsonFuncExpr *jfe = makeNode(JsonFuncExpr); + JsonExpr *je; + JsonTablePlan *plan = jt->plan; + JsonCommon *jscommon; + char *rootPathName = jt->common->pathname; + char *rootPath; + bool is_lateral; + + cxt.pstate = pstate; + cxt.table = jt; + cxt.tablefunc = tf; + cxt.pathNames = NIL; + cxt.pathNameId = 0; + + if (rootPathName) + registerJsonTableColumn(&cxt, rootPathName); + + registerAllJsonTableColumns(&cxt, jt->columns); + +#if 0 /* XXX it' unclear from the standard whether + * root path name is mandatory or not */ + if (plan && plan->plan_type != JSTP_DEFAULT && !rootPathName) + { + /* Assign root path name and create corresponding plan node */ + JsonTablePlan *rootNode = makeNode(JsonTablePlan); + JsonTablePlan *rootPlan = (JsonTablePlan *) + makeJsonTableJoinedPlan(JSTPJ_OUTER, (Node *) rootNode, + (Node *) plan, jt->location); + + rootPathName = generateJsonTablePathName(&cxt); + + rootNode->plan_type = JSTP_SIMPLE; + rootNode->pathname = rootPathName; + + plan = rootPlan; + } +#endif + + jscommon = copyObject(jt->common); + jscommon->pathspec = makeStringConst(pstrdup("$"), -1); + + jfe->op = JSON_TABLE_OP; + jfe->common = jscommon; + jfe->on_error = jt->on_error; + jfe->location = jt->common->location; + + /* + * We make lateral_only names of this level visible, whether or not the + * RangeTableFunc is explicitly marked LATERAL. This is needed for SQL + * spec compliance and seems useful on convenience grounds for all + * functions in FROM. + * + * (LATERAL can't nest within a single pstate level, so we don't need + * save/restore logic here.) + */ + Assert(!pstate->p_lateral_active); + pstate->p_lateral_active = true; + + tf->functype = TFT_JSON_TABLE; + tf->docexpr = transformExpr(pstate, (Node *) jfe, EXPR_KIND_FROM_FUNCTION); + + cxt.contextItemTypid = exprType(tf->docexpr); + + if (!IsA(jt->common->pathspec, A_Const) || + castNode(A_Const, jt->common->pathspec)->val.node.type != T_String) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("only string constants supported in JSON_TABLE path specification"), + parser_errposition(pstate, + exprLocation(jt->common->pathspec)))); + + rootPath = castNode(A_Const, jt->common->pathspec)->val.sval.sval; + + tf->plan = (Node *) transformJsonTableColumns(&cxt, plan, jt->columns, + rootPath, &rootPathName, + jt->common->location); + + /* Also save a copy of the PASSING arguments in the TableFunc node. */ + je = (JsonExpr *) tf->docexpr; + tf->passingvalexprs = copyObject(je->passing_values); + + tf->ordinalitycol = -1; /* undefine ordinality column number */ + tf->location = jt->location; + + pstate->p_lateral_active = false; + + /* + * Mark the RTE as LATERAL if the user said LATERAL explicitly, or if + * there are any lateral cross-references in it. + */ + is_lateral = jt->lateral || contain_vars_of_level((Node *) tf, 0); + + return addRangeTableEntryForTableFunc(pstate, + tf, jt->alias, is_lateral, true); +} diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 864ea9b0d5..3ac7e113f0 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -2073,7 +2073,8 @@ addRangeTableEntryForTableFunc(ParseState *pstate, Assert(list_length(tf->coltypmods) == list_length(tf->colnames)); Assert(list_length(tf->colcollations) == list_length(tf->colnames)); - refname = alias ? alias->aliasname : pstrdup("xmltable"); + refname = alias ? alias->aliasname : + pstrdup(tf->functype == TFT_XMLTABLE ? "xmltable" : "json_table"); rte->rtekind = RTE_TABLEFUNC; rte->relid = InvalidOid; @@ -2096,7 +2097,7 @@ addRangeTableEntryForTableFunc(ParseState *pstate, ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), errmsg("%s function has %d columns available but %d columns specified", - "XMLTABLE", + tf->functype == TFT_XMLTABLE ? "XMLTABLE" : "JSON_TABLE", list_length(tf->colnames), numaliases))); rte->eref = eref; diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4f94fc69d6..6500e42485 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1992,6 +1992,9 @@ FigureColnameInternal(Node *node, char **name) case JSON_EXISTS_OP: *name = "json_exists"; return 2; + case JSON_TABLE_OP: + *name = "json_table"; + return 2; } break; default: diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index eded22e194..92d76d5cde 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -61,10 +61,12 @@ #include "catalog/pg_collation.h" #include "catalog/pg_type.h" +#include "executor/execExpr.h" #include "funcapi.h" #include "lib/stringinfo.h" #include "miscadmin.h" #include "nodes/miscnodes.h" +#include "nodes/nodeFuncs.h" #include "regex/regex.h" #include "utils/builtins.h" #include "utils/date.h" @@ -75,6 +77,8 @@ #include "utils/guc.h" #include "utils/json.h" #include "utils/jsonpath.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" #include "utils/timestamp.h" #include "utils/varlena.h" @@ -156,6 +160,61 @@ typedef struct JsonValueListIterator ListCell *next; } JsonValueListIterator; +/* Structures for JSON_TABLE execution */ + +typedef enum JsonTablePlanStateType +{ + JSON_TABLE_SCAN_STATE = 0, + JSON_TABLE_JOIN_STATE +} JsonTablePlanStateType; + +typedef struct JsonTablePlanState +{ + JsonTablePlanStateType type; + + struct JsonTablePlanState *parent; + struct JsonTablePlanState *nested; +} JsonTablePlanState; + +typedef struct JsonTableScanState +{ + JsonTablePlanState plan; + + MemoryContext mcxt; + JsonPath *path; + List *args; + JsonValueList found; + JsonValueListIterator iter; + Datum current; + int ordinal; + bool currentIsNull; + bool outerJoin; + bool errorOnError; + bool advanceNested; + bool reset; +} JsonTableScanState; + +typedef struct JsonTableJoinState +{ + JsonTablePlanState plan; + + JsonTablePlanState *left; + JsonTablePlanState *right; + bool cross; + bool advanceRight; +} JsonTableJoinState; + +/* random number to identify JsonTableExecContext */ +#define JSON_TABLE_EXEC_CONTEXT_MAGIC 418352867 + +typedef struct JsonTableExecContext +{ + int magic; + JsonTableScanState **colexprscans; + JsonTableScanState *root; + bool empty; +} JsonTableExecContext; + /* strict/lax flags is decomposed into four [un]wrap/error flags */ #define jspStrictAbsenseOfErrors(cxt) (!(cxt)->laxMode) #define jspAutoUnwrap(cxt) ((cxt)->laxMode) @@ -248,6 +307,7 @@ static JsonPathExecResult getArrayIndex(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbValue *jb, int32 *index); static JsonBaseObjectInfo setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id); +static void JsonValueListClear(JsonValueList *jvl); static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv); static int JsonValueListLength(const JsonValueList *jvl); static bool JsonValueListIsEmpty(JsonValueList *jvl); @@ -265,6 +325,14 @@ static JsonbValue *wrapItemsInArray(const JsonValueList *items); static int compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, bool useTz, bool *cast_error); + +static JsonTablePlanState *JsonTableInitPlanState(JsonTableExecContext *cxt, + Node *plan, + JsonTablePlanState *parent); +static bool JsonTablePlanNextRow(JsonTablePlanState *state); +static bool JsonTableScanNextRow(JsonTableScanState *scan); + + /****************** User interface to JsonPath executor ********************/ /* @@ -2518,6 +2586,13 @@ setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id) return baseObject; } +static void +JsonValueListClear(JsonValueList *jvl) +{ + jvl->singleton = NULL; + jvl->list = NULL; +} + static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv) { @@ -3123,3 +3198,471 @@ JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); } } + +/************************ JSON_TABLE functions ***************************/ + +/* + * Returns private data from executor state. Ensure validity by check with + * MAGIC number. + */ +static inline JsonTableExecContext * +GetJsonTableExecContext(TableFuncScanState *state, const char *fname) +{ + JsonTableExecContext *result; + + if (!IsA(state, TableFuncScanState)) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + result = (JsonTableExecContext *) state->opaque; + if (result->magic != JSON_TABLE_EXEC_CONTEXT_MAGIC) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + + return result; +} + +/* Recursively initialize JSON_TABLE scan / join state */ +static JsonTableJoinState * +JsonTableInitJoinState(JsonTableExecContext *cxt, JsonTableSibling *plan, + JsonTablePlanState *parent) +{ + JsonTableJoinState *join = palloc0(sizeof(*join)); + + join->plan.type = JSON_TABLE_JOIN_STATE; + /* parent and nested not set. */ + + join->cross = plan->cross; + join->left = JsonTableInitPlanState(cxt, plan->larg, parent); + join->right = JsonTableInitPlanState(cxt, plan->rarg, parent); + + return join; +} + +static JsonTableScanState * +JsonTableInitScanState(JsonTableExecContext *cxt, JsonTableParent *plan, + JsonTablePlanState *parent, + List *args, MemoryContext mcxt) +{ + JsonTableScanState *scan = palloc0(sizeof(*scan)); + int i; + + scan->plan.type = JSON_TABLE_SCAN_STATE; + scan->plan.parent = parent; + + scan->outerJoin = plan->outerJoin; + scan->errorOnError = plan->errorOnError; + scan->path = DatumGetJsonPathP(plan->path->value->constvalue); + scan->args = args; + scan->mcxt = AllocSetContextCreate(mcxt, "JsonTableExecContext", + ALLOCSET_DEFAULT_SIZES); + + /* + * Set after settings scan->args and scan->mcxt, because the recursive call + * wants to use those values. + */ + scan->plan.nested = plan->child ? + JsonTableInitPlanState(cxt, plan->child, (JsonTablePlanState *) scan) : + NULL; + + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + + for (i = plan->colMin; i <= plan->colMax; i++) + cxt->colexprscans[i] = scan; + + return scan; +} + +/* Recursively initialize JSON_TABLE scan state */ +static JsonTablePlanState * +JsonTableInitPlanState(JsonTableExecContext *cxt, Node *plan, + JsonTablePlanState *parent) +{ + JsonTablePlanState *state; + + if (IsA(plan, JsonTableSibling)) + { + JsonTableSibling *join = castNode(JsonTableSibling, plan); + + state = (JsonTablePlanState *) + JsonTableInitJoinState(cxt, join, parent); + } + else + { + JsonTableParent *scan = castNode(JsonTableParent, plan); + JsonTableScanState *parent_scan = (JsonTableScanState *) parent; + + Assert(parent_scan); + state = (JsonTablePlanState *) + JsonTableInitScanState(cxt, scan, parent, parent_scan->args, + parent_scan->mcxt); + } + + return state; +} + +/* + * JsonTableInitOpaque + * Fill in TableFuncScanState->opaque for JsonTable processor + */ +static void +JsonTableInitOpaque(TableFuncScanState *state, int natts) +{ + JsonTableExecContext *cxt; + PlanState *ps = &state->ss.ps; + TableFuncScan *tfs = castNode(TableFuncScan, ps->plan); + TableFunc *tf = tfs->tablefunc; + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + JsonExpr *je = castNode(JsonExpr, tf->docexpr); + List *args = NIL; + + cxt = palloc0(sizeof(JsonTableExecContext)); + cxt->magic = JSON_TABLE_EXEC_CONTEXT_MAGIC; + + if (state->passingvalexprs) + { + ListCell *exprlc; + ListCell *namelc; + + Assert(list_length(state->passingvalexprs) == + list_length(je->passing_names)); + forboth(exprlc, state->passingvalexprs, + namelc, je->passing_names) + { + ExprState *state = lfirst_node(ExprState, exprlc); + String *name = lfirst_node(String, namelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(name->sval); + var->typid = exprType((Node *) state->expr); + var->typmod = exprTypmod((Node *) state->expr); + + /* + * Evaluate the expression and save the value to be returned by + * GetJsonPathVar(). + */ + var->value = ExecEvalExpr(state, ps->ps_ExprContext, + &var->isnull); + + args = lappend(args, var); + } + } + + cxt->colexprscans = palloc(sizeof(JsonTableScanState *) * + list_length(tf->colvalexprs)); + + cxt->root = JsonTableInitScanState(cxt, root, NULL, args, + CurrentMemoryContext); + state->opaque = cxt; +} + +/* Reset scan iterator to the beginning of the item list */ +static void +JsonTableRescan(JsonTableScanState *scan) +{ + JsonValueListInitIterator(&scan->found, &scan->iter); + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + scan->advanceNested = false; + scan->ordinal = 0; +} + +/* Reset context item of a scan, execute JSON path and reset a scan */ +static void +JsonTableResetContextItem(JsonTableScanState *scan, Datum item) +{ + MemoryContext oldcxt; + JsonPathExecResult res; + Jsonb *js = (Jsonb *) DatumGetJsonbP(item); + + JsonValueListClear(&scan->found); + + MemoryContextResetOnly(scan->mcxt); + + oldcxt = MemoryContextSwitchTo(scan->mcxt); + + res = executeJsonPath(scan->path, scan->args, GetJsonPathVar, js, + scan->errorOnError, &scan->found, + false /* FIXME */ ); + + MemoryContextSwitchTo(oldcxt); + + if (jperIsError(res)) + { + Assert(!scan->errorOnError); + JsonValueListClear(&scan->found); /* EMPTY ON ERROR case */ + } + + JsonTableRescan(scan); +} + +/* + * JsonTableSetDocument + * Install the input document + */ +static void +JsonTableSetDocument(TableFuncScanState *state, Datum value) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableSetDocument"); + + JsonTableResetContextItem(cxt->root, value); +} + +/* Recursively reset scan and its child nodes */ +static void +JsonTableRescanRecursive(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTableRescanRecursive(join->left); + JsonTableRescanRecursive(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan = (JsonTableScanState *) state; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + JsonTableRescan(scan); + if (scan->plan.nested) + JsonTableRescanRecursive(scan->plan.nested); + } +} + +/* + * Fetch next row from a cross/union joined scan. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTablePlanNextRow(JsonTablePlanState *state) +{ + JsonTableJoinState *join; + + if (state->type == JSON_TABLE_SCAN_STATE) + return JsonTableScanNextRow((JsonTableScanState *) state); + + join = (JsonTableJoinState *) state; + if (join->advanceRight) + { + /* fetch next inner row */ + if (JsonTablePlanNextRow(join->right)) + return true; + + /* inner rows are exhausted */ + if (join->cross) + join->advanceRight = false; /* next outer row */ + else + return false; /* end of scan */ + } + + while (!join->advanceRight) + { + /* fetch next outer row */ + bool left = JsonTablePlanNextRow(join->left); + + if (join->cross) + { + if (!left) + return false; /* end of scan */ + + JsonTableRescanRecursive(join->right); + + if (!JsonTablePlanNextRow(join->right)) + continue; /* next outer row */ + + join->advanceRight = true; /* next inner row */ + } + else if (!left) + { + if (!JsonTablePlanNextRow(join->right)) + return false; /* end of scan */ + + join->advanceRight = true; /* next inner row */ + } + + break; + } + + return true; +} + +/* Recursively set 'reset' flag of scan and its child nodes */ +static void +JsonTablePlanReset(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTablePlanReset(join->left); + JsonTablePlanReset(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + scan = (JsonTableScanState *) state; + scan->reset = true; + scan->advanceNested = false; + + if (scan->plan.nested) + JsonTablePlanReset(scan->plan.nested); + } +} + +/* + * Fetch next row from a simple scan with outer/inner joined nested subscans. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTableScanNextRow(JsonTableScanState *scan) +{ + /* reset context item if requested */ + if (scan->reset) + { + JsonTableScanState *parent_scan = + (JsonTableScanState *) scan->plan.parent; + + Assert(parent_scan && !parent_scan->currentIsNull); + JsonTableResetContextItem(scan, parent_scan->current); + scan->reset = false; + } + + if (scan->advanceNested) + { + /* fetch next nested row */ + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested) + return true; + } + + for (;;) + { + /* fetch next row */ + JsonbValue *jbv = JsonValueListNext(&scan->found, &scan->iter); + MemoryContext oldcxt; + + if (!jbv) + { + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + return false; /* end of scan */ + } + + /* set current row item */ + oldcxt = MemoryContextSwitchTo(scan->mcxt); + scan->current = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + scan->currentIsNull = false; + MemoryContextSwitchTo(oldcxt); + + scan->ordinal++; + + if (!scan->plan.nested) + break; + + JsonTablePlanReset(scan->plan.nested); + + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested || scan->outerJoin) + break; + } + + return true; +} + +/* + * JsonTableFetchRow + * Prepare the next "current" tuple for upcoming GetValue calls. + * Returns FALSE if the row-filter expression returned no more rows. + */ +static bool +JsonTableFetchRow(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableFetchRow"); + + if (cxt->empty) + return false; + + return JsonTableScanNextRow(cxt->root); +} + +/* + * JsonTableGetValue + * Return the value for column number 'colnum' for the current row. + * + * This leaks memory, so be sure to reset often the context in which it's + * called. + */ +static Datum +JsonTableGetValue(TableFuncScanState *state, int colnum, + Oid typid, int32 typmod, bool *isnull) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableGetValue"); + ExprContext *econtext = state->ss.ps.ps_ExprContext; + ExprState *estate = list_nth(state->colvalexprs, colnum); + JsonTableScanState *scan = cxt->colexprscans[colnum]; + Datum result; + + if (scan->currentIsNull) /* NULL from outer/union join */ + { + result = (Datum) 0; + *isnull = true; + } + else if (estate) /* regular column */ + { + Datum saved_caseValue = econtext->caseValue_datum; + bool saved_caseIsNull = econtext->caseValue_isNull; + + /* Pass the value for CaseTestExpr that may be present in colexpr */ + econtext->caseValue_datum = scan->current; + econtext->caseValue_isNull = false; + + result = ExecEvalExpr(estate, econtext, isnull); + + econtext->caseValue_datum = saved_caseValue; + econtext->caseValue_isNull = saved_caseIsNull; + } + else + { + result = Int32GetDatum(scan->ordinal); /* ordinality column */ + *isnull = false; + } + + return result; +} + +/* + * JsonTableDestroyOpaque + */ +static void +JsonTableDestroyOpaque(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableDestroyOpaque"); + + /* not valid anymore */ + cxt->magic = 0; + + state->opaque = NULL; +} + +const TableFuncRoutine JsonbTableRoutine = +{ + JsonTableInitOpaque, + JsonTableSetDocument, + NULL, + NULL, + NULL, + JsonTableFetchRow, + JsonTableGetValue, + JsonTableDestroyOpaque +}; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d1ec418ccf..e89d1e03d6 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -522,6 +522,8 @@ static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); static void get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit); +static void get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8606,7 +8608,8 @@ get_json_behavior(JsonBehavior *behavior, deparse_context *context, /* * get_json_expr_options * - * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS and + * JSON_TABLE columns. */ static void get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, @@ -9854,6 +9857,9 @@ get_rule_expr(Node *node, deparse_context *context, case JSON_EXISTS_OP: appendStringInfoString(buf, "JSON_EXISTS("); break; + default: + elog(ERROR, "unexpected JsonExpr type: %d", jexpr->op); + break; } get_rule_expr(jexpr->formatted_expr, context, showimplicit); @@ -11207,16 +11213,14 @@ get_sublink_expr(SubLink *sublink, deparse_context *context) /* ---------- - * get_tablefunc - Parse back a table function + * get_xmltable - Parse back a XMLTABLE function * ---------- */ static void -get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +get_xmltable(TableFunc *tf, deparse_context *context, bool showimplicit) { StringInfo buf = context->buf; - /* XMLTABLE is the only existing implementation. */ - appendStringInfoString(buf, "XMLTABLE("); if (tf->ns_uris != NIL) @@ -11307,6 +11311,271 @@ get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) appendStringInfoChar(buf, ')'); } +/* + * get_json_nested_columns - Parse back nested JSON_TABLE columns + */ +static void +get_json_table_nested_columns(TableFunc *tf, Node *node, + deparse_context *context, bool showimplicit, + bool needcomma) +{ + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_nested_columns(tf, n->larg, context, showimplicit, + needcomma); + get_json_table_nested_columns(tf, n->rarg, context, showimplicit, true); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + if (needcomma) + appendStringInfoChar(context->buf, ','); + + appendStringInfoChar(context->buf, ' '); + appendContextKeyword(context, "NESTED PATH ", 0, 0, 0); + get_const_expr(n->path->value, context, -1); + appendStringInfo(context->buf, " AS %s", quote_identifier(n->path->name)); + get_json_table_columns(tf, n, context, showimplicit); + } +} + +/* + * get_json_table_plan - Parse back a JSON_TABLE plan + */ +static void +get_json_table_plan(TableFunc *tf, Node *node, deparse_context *context, + bool parenthesize) +{ + if (parenthesize) + appendStringInfoChar(context->buf, '('); + + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_plan(tf, n->larg, context, + IsA(n->larg, JsonTableSibling) || + castNode(JsonTableParent, n->larg)->child); + + appendStringInfoString(context->buf, n->cross ? " CROSS " : " UNION "); + + get_json_table_plan(tf, n->rarg, context, + IsA(n->rarg, JsonTableSibling) || + castNode(JsonTableParent, n->rarg)->child); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + appendStringInfoString(context->buf, quote_identifier(n->path->name)); + + if (n->child) + { + appendStringInfoString(context->buf, + n->outerJoin ? " OUTER " : " INNER "); + get_json_table_plan(tf, n->child, context, + IsA(n->child, JsonTableSibling)); + } + } + + if (parenthesize) + appendStringInfoChar(context->buf, ')'); +} + +/* + * get_json_table_columns - Parse back JSON_TABLE columns + */ +static void +get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + ListCell *lc_colname; + ListCell *lc_coltype; + ListCell *lc_coltypmod; + ListCell *lc_colvalexpr; + int colnum = 0; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "COLUMNS (", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forfour(lc_colname, tf->colnames, + lc_coltype, tf->coltypes, + lc_coltypmod, tf->coltypmods, + lc_colvalexpr, tf->colvalexprs) + { + char *colname = strVal(lfirst(lc_colname)); + JsonExpr *colexpr; + Oid typid; + int32 typmod; + bool ordinality; + JsonBehaviorType default_behavior; + + typid = lfirst_oid(lc_coltype); + typmod = lfirst_int(lc_coltypmod); + colexpr = castNode(JsonExpr, lfirst(lc_colvalexpr)); + + if (colnum < node->colMin) + { + colnum++; + continue; + } + + if (colnum > node->colMax) + break; + + if (colnum > node->colMin) + appendStringInfoString(buf, ", "); + + colnum++; + + ordinality = !colexpr; + + appendContextKeyword(context, "", 0, 0, 0); + + appendStringInfo(buf, "%s %s", quote_identifier(colname), + ordinality ? "FOR ORDINALITY" : + format_type_with_typemod(typid, typmod)); + if (ordinality) + continue; + + if (colexpr->op == JSON_EXISTS_OP) + { + appendStringInfoString(buf, " EXISTS"); + default_behavior = JSON_BEHAVIOR_FALSE; + } + else + { + if (colexpr->op == JSON_QUERY_OP) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(typid, &typcategory, &typispreferred); + + if (typcategory == TYPCATEGORY_STRING) + appendStringInfoString(buf, + colexpr->format->format_type == JS_FORMAT_JSONB ? + " FORMAT JSONB" : " FORMAT JSON"); + } + + default_behavior = JSON_BEHAVIOR_NULL; + } + + if (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR) + default_behavior = JSON_BEHAVIOR_ERROR; + + appendStringInfoString(buf, " PATH "); + + get_json_path_spec(colexpr->path_spec, context, showimplicit); + + get_json_expr_options(colexpr, context, default_behavior); + } + + if (node->child) + get_json_table_nested_columns(tf, node->child, context, showimplicit, + node->colMax >= node->colMin); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_json_table - Parse back a JSON_TABLE function + * ---------- + */ +static void +get_json_table(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + + appendStringInfoString(buf, "JSON_TABLE("); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_const_expr(root->path->value, context, -1); + + appendStringInfo(buf, " AS %s", quote_identifier(root->path->name)); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PASSING ", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr((Node *) lfirst(lc2), context, false); + appendStringInfo(buf, " AS %s", + quote_identifier((lfirst_node(String, lc1))->sval) + ); + } + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + } + + get_json_table_columns(tf, root, context, showimplicit); + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PLAN ", 0, 0, 0); + get_json_table_plan(tf, (Node *) root, context, true); + + if (jexpr->on_error->btype != JSON_BEHAVIOR_EMPTY) + get_json_behavior(jexpr->on_error, context, "ERROR"); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_tablefunc - Parse back a table function + * ---------- + */ +static void +get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + /* XMLTABLE and JSON_TABLE are the only existing implementations. */ + + if (tf->functype == TFT_XMLTABLE) + get_xmltable(tf, context, showimplicit); + else if (tf->functype == TFT_JSON_TABLE) + get_json_table(tf, context, showimplicit); +} + /* ---------- * get_from_clause - Parse back a FROM clause * diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 584bf7001a..91453681e3 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1879,6 +1879,8 @@ typedef struct TableFuncScanState ExprState *rowexpr; /* state for row-generating expression */ List *colexprs; /* state for column-generating expression */ List *coldefexprs; /* state for column default expressions */ + List *colvalexprs; /* state for column value expression */ + List *passingvalexprs; /* state for PASSING argument expression */ List *ns_names; /* same as TableFunc.ns_names */ List *ns_uris; /* list of states of namespace URI exprs */ Bitmapset *notnulls; /* nullability flag for each output column */ diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 5e149b0266..d8466a2699 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -112,6 +112,8 @@ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *expr, JsonFormat *format); extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); +extern Node *makeJsonTableJoinedPlan(JsonTablePlanJoinType type, + Node *plan1, Node *plan2, int location); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 7d63a37d5f..b15f71cc92 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1733,6 +1733,19 @@ typedef enum JsonQuotes */ typedef char *JsonPathSpec; +/* + * JsonTableColumnType - + * enumeration of JSON_TABLE column types + */ +typedef enum JsonTableColumnType +{ + JTC_FOR_ORDINALITY, + JTC_REGULAR, + JTC_EXISTS, + JTC_FORMATTED, + JTC_NESTED, +} JsonTableColumnType; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1786,6 +1799,83 @@ typedef struct JsonFuncExpr int location; /* token location, or -1 if unknown */ } JsonFuncExpr; +/* + * JsonTableColumn - + * untransformed representation of JSON_TABLE column + */ +typedef struct JsonTableColumn +{ + NodeTag type; + JsonTableColumnType coltype; /* column type */ + char *name; /* column name */ + TypeName *typeName; /* column type name */ + char *pathspec; /* path specification, if any */ + char *pathname; /* path name, if any */ + JsonFormat *format; /* JSON format clause, if specified */ + JsonWrapper wrapper; /* WRAPPER behavior for formatted columns */ + bool omit_quotes; /* omit or keep quotes on scalar strings? */ + List *columns; /* nested columns */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + int location; /* token location, or -1 if unknown */ +} JsonTableColumn; + +/* + * JsonTablePlanType - + * flags for JSON_TABLE plan node types representation + */ +typedef enum JsonTablePlanType +{ + JSTP_DEFAULT, + JSTP_SIMPLE, + JSTP_JOINED, +} JsonTablePlanType; + +/* + * JsonTablePlanJoinType - + * flags for JSON_TABLE join types representation + */ +typedef enum JsonTablePlanJoinType +{ + JSTPJ_INNER = 0x01, + JSTPJ_OUTER = 0x02, + JSTPJ_CROSS = 0x04, + JSTPJ_UNION = 0x08, +} JsonTablePlanJoinType; + +typedef struct JsonTablePlan JsonTablePlan; + +/* + * JsonTablePlan - + * untransformed representation of JSON_TABLE plan node + */ +struct JsonTablePlan +{ + NodeTag type; + JsonTablePlanType plan_type; /* plan type */ + JsonTablePlanJoinType join_type; /* join type (for joined plan only) */ + JsonTablePlan *plan1; /* first joined plan */ + JsonTablePlan *plan2; /* second joined plan */ + char *pathname; /* path name (for simple plan only) */ + int location; /* token location, or -1 if unknown */ +}; + +/* + * JsonTable - + * untransformed representation of JSON_TABLE + */ +typedef struct JsonTable +{ + NodeTag type; + JsonCommon *common; /* common JSON path syntax fields */ + List *columns; /* list of JsonTableColumn */ + JsonTablePlan *plan; /* join plan, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + Alias *alias; /* table alias in FROM clause */ + bool lateral; /* does it have LATERAL prefix? */ + int location; /* token location, or -1 if unknown */ +} JsonTable; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 3937114743..cea15cc4e5 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -86,8 +86,14 @@ typedef struct RangeVar int location; } RangeVar; +typedef enum TableFuncType +{ + TFT_XMLTABLE, + TFT_JSON_TABLE +} TableFuncType; + /* - * TableFunc - node for a table function, such as XMLTABLE. + * TableFunc - node for a table function, such as XMLTABLE or JSON_TABLE. * * Entries in the ns_names list are either String nodes containing * literal namespace names, or NULL pointers to represent DEFAULT. @@ -95,6 +101,8 @@ typedef struct RangeVar typedef struct TableFunc { NodeTag type; + /* XMLTABLE or JSON_TABLE */ + TableFuncType functype; /* list of namespace URI expressions */ List *ns_uris pg_node_attr(query_jumble_ignore); /* list of namespace names or NULL */ @@ -115,8 +123,14 @@ typedef struct TableFunc List *colexprs; /* list of column default expressions */ List *coldefexprs pg_node_attr(query_jumble_ignore); + /* list of column value expressions */ + List *colvalexprs pg_node_attr(query_jumble_ignore); + /* list of PASSING argument expressions */ + List *passingvalexprs pg_node_attr(query_jumble_ignore); /* nullability flag for each output column */ Bitmapset *notnulls pg_node_attr(query_jumble_ignore); + /* JSON_TABLE plan */ + Node *plan pg_node_attr(query_jumble_ignore); /* counts from 0; -1 if none specified */ int ordinalitycol pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ @@ -1552,7 +1566,8 @@ typedef enum JsonExprOp { JSON_VALUE_OP, /* JSON_VALUE() */ JSON_QUERY_OP, /* JSON_QUERY() */ - JSON_EXISTS_OP /* JSON_EXISTS() */ + JSON_EXISTS_OP, /* JSON_EXISTS() */ + JSON_TABLE_OP /* JSON_TABLE() */ } JsonExprOp; /* @@ -1770,6 +1785,48 @@ typedef struct JsonExpr int location; /* token location, or -1 if unknown */ } JsonExpr; +/* + * JsonTablePath + * A JSON path expression to be computed as part of evaluating + * a JSON_TABLE plan node + */ +typedef struct JsonTablePath +{ + NodeTag type; + + Const *value; + char *name; +} JsonTablePath; + +/* + * JsonTableParent - + * transformed representation of parent JSON_TABLE plan node + */ +typedef struct JsonTableParent +{ + NodeTag type; + JsonTablePath *path; + Node *child; /* nested columns, if any */ + bool outerJoin; /* outer or inner join for nested columns? */ + int colMin; /* min column index in the resulting column + * list */ + int colMax; /* max column index in the resulting column + * list */ + bool errorOnError; /* ERROR/EMPTY ON ERROR behavior */ +} JsonTableParent; + +/* + * JsonTableSibling - + * transformed representation of joined sibling JSON_TABLE plan node + */ +typedef struct JsonTableSibling +{ + NodeTag type; + Node *larg; /* left join node */ + Node *rarg; /* right join node */ + bool cross; /* cross or union join? */ +} JsonTableSibling; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0954d9fc7b..f88a6c9ac6 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -242,6 +242,7 @@ PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_table", JSON_TABLE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) @@ -284,6 +285,7 @@ PG_KEYWORD("names", NAMES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("national", NATIONAL, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("natural", NATURAL, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("nchar", NCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("nested", NESTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("new", NEW, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("next", NEXT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("nfc", NFC, UNRESERVED_KEYWORD, BARE_LABEL) @@ -334,7 +336,9 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h index 8e26482db4..7152a8ef48 100644 --- a/src/include/parser/parse_clause.h +++ b/src/include/parser/parse_clause.h @@ -51,4 +51,7 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle, extern Index assignSortGroupRef(TargetEntry *tle, List *tlist); extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList); +/* functions in parse_jsontable.c */ +extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt); + #endif /* PARSE_CLAUSE_H */ diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index 4dae78a98c..b7ada77cb1 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -15,6 +15,7 @@ #define JSONPATH_H #include "fmgr.h" +#include "executor/tablefunc.h" #include "nodes/pg_list.h" #include "nodes/primnodes.h" #include "utils/jsonb.h" @@ -288,4 +289,6 @@ extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars); +extern PGDLLIMPORT const TableFuncRoutine JsonbTableRoutine; + #endif diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out index 8b87580752..995f267404 100644 --- a/src/test/regress/expected/json_sqljson.out +++ b/src/test/regress/expected/json_sqljson.out @@ -16,3 +16,9 @@ ERROR: JSON_QUERY() is not yet implemented for the json type LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); ^ HINT: Try casting the argument to jsonb +-- JSON_TABLE +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); +ERROR: JSON_TABLE() is not yet implemented for the json type +LINE 1: SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo ... + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out index 22f8679917..4323ed6b35 100644 --- a/src/test/regress/expected/jsonb_sqljson.out +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -1040,3 +1040,1072 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.dateti ERROR: functions in index expression must be marked IMMUTABLE CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; +-- JSON_TABLE +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); +ERROR: syntax error at or near "(" +LINE 1: SELECT JSON_TABLE('[]', '$'); + ^ +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); +ERROR: syntax error at or near ")" +LINE 1: SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + ^ +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); +ERROR: JSON_TABLE function has 1 columns available but 2 columns specified +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + foo +----- +(0 rows) + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + item | foo +------+----- + 123 | +(1 row) + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + js | id | id2 | int | text | char(4) | bool | numeric | domain | js | jb | jst | jsc | jsv | jsb | jsbq | aaa | aaa1 | exists1 | exists2 | exists3 | exists4 | js2 | jsb2w | jsb2q | ia | ta | jba +---------------------------------------------------------------------------------------+----+-----+-----+---------+---------+------+---------+---------+--------------+--------------+--------------+------+------+--------------+--------------+-----+------+---------+---------+---------+---------+--------------+----------------+--------------+----+----+----- + 1 | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [] | | | | | | | | | | | | | | | | | | | | | | | | | | | + {} | 1 | 1 | | | | | | | {} | {} | {} | {} | {} | {} | {} | | | f | 0 | | false | {} | [{}] | {} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 2 | 2 | 1 | 1.23 | 1.23 | | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | | | f | 0 | | false | 1.23 | [1.23] | 1.23 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 3 | 3 | 2 | 2 | 2 | | 2 | 2 | "2" | "2" | "2" | "2" | "2" | "2" | 2 | | | f | 0 | | false | "2" | ["2"] | 2 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 4 | 4 | | aaaaaaa | aaaa | | | aaaaaaa | "aaaaaaa" | "aaaaaaa" | "aaaaaaa" | "aaa | "aaa | "aaaaaaa" | | | | f | 0 | | false | "aaaaaaa" | ["aaaaaaa"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 5 | 5 | | foo | foo | | | | "foo" | "foo" | "foo" | "foo | "foo | "foo" | | | | f | 0 | | false | "foo" | ["foo"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 6 | 6 | | | | | | | null | null | null | null | null | null | null | | | f | 0 | | false | null | [null] | null | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 7 | 7 | 0 | false | fals | f | | false | false | false | false | fals | fals | false | false | | | f | 0 | | false | false | [false] | false | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 8 | 8 | 1 | true | true | t | | true | true | true | true | true | true | true | true | | | f | 0 | | false | true | [true] | true | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 9 | 9 | | | | | | | {"aaa": 123} | {"aaa": 123} | {"aaa": 123} | {"aa | {"aa | {"aaa": 123} | {"aaa": 123} | 123 | 123 | t | 1 | 1 | true | {"aaa": 123} | [{"aaa": 123}] | {"aaa": 123} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 10 | 10 | | [1,2] | [1,2 | | | [1,2] | "[1,2]" | "[1,2]" | "[1,2]" | "[1, | "[1, | "[1,2]" | [1, 2] | | | f | 0 | | false | "[1,2]" | ["[1,2]"] | [1, 2] | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 11 | 11 | | "str" | "str | | | "str" | "\"str\"" | "\"str\"" | "\"str\"" | "\"s | "\"s | "\"str\"" | "str" | | | f | 0 | | false | "\"str\"" | ["\"str\""] | "str" | | | +(14 rows) + +-- JSON_TABLE: Test backward parsing +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); +\sv jsonb_table_view +CREATE OR REPLACE VIEW public.jsonb_table_view AS + SELECT id, + id2, + "int", + text, + "char(4)", + bool, + "numeric", + domain, + js, + jb, + jst, + jsc, + jsv, + jsb, + jsbq, + aaa, + aaa1, + exists1, + exists2, + exists3, + js2, + jsb2w, + jsb2q, + ia, + ta, + jba, + a1, + b1, + a11, + a21, + a22 + FROM JSON_TABLE( + 'null'::jsonb, '$[*]' AS json_table_path_1 + PASSING + 1 + 2 AS a, + '"foo"'::json AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, + "int" integer PATH '$', + text text PATH '$', + "char(4)" character(4) PATH '$', + bool boolean PATH '$', + "numeric" numeric PATH '$', + domain jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc character(4) FORMAT JSON PATH '$', + jsv character varying(4) FORMAT JSON PATH '$', + jsb jsonb PATH '$', + jsbq jsonb PATH '$' OMIT QUOTES, + aaa integer PATH '$."aaa"', + aaa1 integer PATH '$."aaa"', + exists1 boolean EXISTS PATH '$."aaa"', + exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia integer[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 + COLUMNS ( + a1 integer PATH '$."a1"', + b1 text PATH '$."b1"', + NESTED PATH '$[*]' AS "p1 1" + COLUMNS ( + a11 text PATH '$."a11"' + ) + ), + NESTED PATH '$[2]' AS p2 + COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" + COLUMNS ( + a21 text PATH '$."a21"' + ), + NESTED PATH '$[*]' AS p22 + COLUMNS ( + a22 text PATH '$."a22"' + ) + ) + ) + PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22)))) + ) +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Table Function Scan on "json_table" + Output: "json_table".id, "json_table".id2, "json_table"."int", "json_table".text, "json_table"."char(4)", "json_table".bool, "json_table"."numeric", "json_table".domain, "json_table".js, "json_table".jb, "json_table".jst, "json_table".jsc, "json_table".jsv, "json_table".jsb, "json_table".jsbq, "json_table".aaa, "json_table".aaa1, "json_table".exists1, "json_table".exists2, "json_table".exists3, "json_table".js2, "json_table".jsb2w, "json_table".jsb2q, "json_table".ia, "json_table".ta, "json_table".jba, "json_table".a1, "json_table".b1, "json_table".a11, "json_table".a21, "json_table".a22 + Table Function Call: JSON_TABLE('null'::jsonb, '$[*]' AS json_table_path_1 PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (id FOR ORDINALITY, id2 FOR ORDINALITY, "int" integer PATH '$', text text PATH '$', "char(4)" character(4) PATH '$', bool boolean PATH '$', "numeric" numeric PATH '$', domain jsonb_test_domain PATH '$', js json PATH '$', jb jsonb PATH '$', jst text FORMAT JSON PATH '$', jsc character(4) FORMAT JSON PATH '$', jsv character varying(4) FORMAT JSON PATH '$', jsb jsonb PATH '$', jsbq jsonb PATH '$' OMIT QUOTES, aaa integer PATH '$."aaa"', aaa1 integer PATH '$."aaa"', exists1 boolean EXISTS PATH '$."aaa"', exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, js2 json PATH '$', jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, jsb2q jsonb PATH '$' OMIT QUOTES, ia integer[] PATH '$', ta text[] PATH '$', jba jsonb[] PATH '$', NESTED PATH '$[1]' AS p1 COLUMNS (a1 integer PATH '$."a1"', b1 text PATH '$."b1"', NESTED PATH '$[*]' AS "p1 1" COLUMNS (a11 text PATH '$."a11"')), NESTED PATH '$[2]' AS p2 COLUMNS ( NESTED PATH '$[*]' AS "p2:1" COLUMNS (a21 text PATH '$."a21"'), NESTED PATH '$[*]' AS p22 COLUMNS (a22 text PATH '$."a22"'))) PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22))))) +(3 rows) + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + js | a +-------+--- + 1 | 1 + "err" | +(2 rows) + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; + a +--- + +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: jsonpath member accessor can only be applied to an object +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: no SQL/JSON item +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 1 +(1 row) + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); + a +--- + 0 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to smallint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to bigint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to real +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 E... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); + a +----- + fal +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to json +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to jsonb +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EX... + ^ +-- JSON_TABLE: nested paths and plans +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb '[]', '$' -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 4: NESTED PATH '$' COLUMNS ( -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: b +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +-- JSON_TABLE: plan validation +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p1) + ^ +DETAIL: Path name mismatch: expected p0 but p1 is given. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 UNION p1 UNION p11) + ^ +DETAIL: Expected INNER or OUTER JSON_TABLE plan node. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 8: NESTED PATH '$' AS p2 COLUMNS ( + ^ +DETAIL: Plan node for nested path p2 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 5: NESTED PATH '$' AS p11 COLUMNS ( foo int ), + ^ +DETAIL: Plan node for nested path p11 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) + ^ +DETAIL: Plan node contains some extra or duplicate sibling nodes. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 6: NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ^ +DETAIL: Plan node for nested path p12 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 9: NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ^ +DETAIL: Plan node for nested path p21 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + bar | foo | baz +-----+-----+----- +(0 rows) + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb 'null', 'strict $[*]' -- without root path name + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- JSON_TABLE: plan execution +CREATE TEMP TABLE jsonb_table_test (js jsonb); +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + n | a | c | b +---+----+----+--- + 1 | 1 | | + 2 | 2 | 10 | + 2 | 2 | | + 2 | 2 | 20 | + 2 | 2 | | 1 + 2 | 2 | | 2 + 2 | 2 | | 3 + 3 | 3 | | 1 + 3 | 3 | | 2 + 4 | -1 | | 1 + 4 | -1 | | 2 +(11 rows) + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + n | a | b | b1 | c | c1 | b +---+---+--------------+-----+------+----+----- + 1 | 1 | [1, 10] | 1 | 1 | | 101 + 1 | 1 | [1, 10] | 1 | null | | 101 + 1 | 1 | [1, 10] | 1 | 2 | | 101 + 1 | 1 | [1, 10] | 10 | 1 | | 110 + 1 | 1 | [1, 10] | 10 | null | | 110 + 1 | 1 | [1, 10] | 10 | 2 | | 110 + 1 | 1 | [2] | 2 | 1 | | 102 + 1 | 1 | [2] | 2 | null | | 102 + 1 | 1 | [2] | 2 | 2 | | 102 + 1 | 1 | [3, 30, 300] | 3 | 1 | | 103 + 1 | 1 | [3, 30, 300] | 3 | null | | 103 + 1 | 1 | [3, 30, 300] | 3 | 2 | | 103 + 1 | 1 | [3, 30, 300] | 30 | 1 | | 130 + 1 | 1 | [3, 30, 300] | 30 | null | | 130 + 1 | 1 | [3, 30, 300] | 30 | 2 | | 130 + 1 | 1 | [3, 30, 300] | 300 | 1 | | 400 + 1 | 1 | [3, 30, 300] | 300 | null | | 400 + 1 | 1 | [3, 30, 300] | 300 | 2 | | 400 + 2 | 2 | | | | | + 3 | | | | | | +(20 rows) + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + x | y | y | z +---+---+--------------+--- + 2 | 1 | [1, 2, 3] | 1 + 2 | 1 | [1, 2, 3] | 2 + 2 | 1 | [1, 2, 3] | 3 + 3 | 1 | [1, 2, 3] | 1 + 3 | 1 | [1, 2, 3] | 2 + 3 | 1 | [1, 2, 3] | 3 + 3 | 1 | [2, 3, 4, 5] | 2 + 3 | 1 | [2, 3, 4, 5] | 3 + 3 | 1 | [2, 3, 4, 5] | 4 + 3 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [1, 2, 3] | 1 + 4 | 1 | [1, 2, 3] | 2 + 4 | 1 | [1, 2, 3] | 3 + 4 | 1 | [2, 3, 4, 5] | 2 + 4 | 1 | [2, 3, 4, 5] | 3 + 4 | 1 | [2, 3, 4, 5] | 4 + 4 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [3, 4, 5, 6] | 3 + 4 | 1 | [3, 4, 5, 6] | 4 + 4 | 1 | [3, 4, 5, 6] | 5 + 4 | 1 | [3, 4, 5, 6] | 6 + 2 | 2 | [1, 2, 3] | 2 + 2 | 2 | [1, 2, 3] | 3 + 3 | 2 | [1, 2, 3] | 2 + 3 | 2 | [1, 2, 3] | 3 + 3 | 2 | [2, 3, 4, 5] | 2 + 3 | 2 | [2, 3, 4, 5] | 3 + 3 | 2 | [2, 3, 4, 5] | 4 + 3 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [1, 2, 3] | 2 + 4 | 2 | [1, 2, 3] | 3 + 4 | 2 | [2, 3, 4, 5] | 2 + 4 | 2 | [2, 3, 4, 5] | 3 + 4 | 2 | [2, 3, 4, 5] | 4 + 4 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [3, 4, 5, 6] | 3 + 4 | 2 | [3, 4, 5, 6] | 4 + 4 | 2 | [3, 4, 5, 6] | 5 + 4 | 2 | [3, 4, 5, 6] | 6 + 2 | 3 | [1, 2, 3] | 3 + 3 | 3 | [1, 2, 3] | 3 + 3 | 3 | [2, 3, 4, 5] | 3 + 3 | 3 | [2, 3, 4, 5] | 4 + 3 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [1, 2, 3] | 3 + 4 | 3 | [2, 3, 4, 5] | 3 + 4 | 3 | [2, 3, 4, 5] | 4 + 4 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [3, 4, 5, 6] | 3 + 4 | 3 | [3, 4, 5, 6] | 4 + 4 | 3 | [3, 4, 5, 6] | 5 + 4 | 3 | [3, 4, 5, 6] | 6 +(52 rows) + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; +ERROR: could not find jsonpath variable "x" +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); + json_value +------------ + foo +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_query +------------ + 123 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); + json_query +------------ + [123] +(1 row) + +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +ERROR: syntax error at or near " " of jsonpath input +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); +ERROR: only string constants supported in JSON_TABLE path specification +LINE 1: SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || '... + ^ diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql index 4f30fa46b9..df4a430d88 100644 --- a/src/test/regress/sql/json_sqljson.sql +++ b/src/test/regress/sql/json_sqljson.sql @@ -9,3 +9,7 @@ SELECT JSON_VALUE(NULL FORMAT JSON, '$'); -- JSON_QUERY SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + +-- JSON_TABLE + +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql index b8a6148b7b..fbd86f2084 100644 --- a/src/test/regress/sql/jsonb_sqljson.sql +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -325,3 +325,632 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; + +-- JSON_TABLE + +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); + +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); + +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); + +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + +-- JSON_TABLE: Test backward parsing + +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); + +\sv jsonb_table_view + +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; + +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); + +-- JSON_TABLE: nested paths and plans + +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; + +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; + +-- JSON_TABLE: plan validation + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; + +-- JSON_TABLE: plan execution + +CREATE TEMP TABLE jsonb_table_test (js jsonb); + +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); + +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; + +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 6e4c026492..642a29d8d5 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1303,6 +1303,7 @@ JsonPathKeyword JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback +JsonPathSpec JsonPathString JsonPathVarCallback JsonPathVariable @@ -1311,6 +1312,17 @@ JsonQuotes JsonReturning JsonSemAction JsonSerializeExpr +JsonTable +JsonTableColumn +JsonTableColumnType +JsonTableContext +JsonTableJoinState +JsonTableParent +JsonTablePlan +JsonTablePlanJoinType +JsonTablePlanType +JsonTableScanState +JsonTableSibling JsonTokenType JsonTransformStringValuesAction JsonTypeCategory @@ -2765,6 +2777,7 @@ TableFunc TableFuncRoutine TableFuncScan TableFuncScanState +TableFuncType TableInfo TableLikeClause TableSampleClause -- 2.35.3
v7-0005-Claim-SQL-standard-compliance-for-SQL-JSON-featur.patch (application/octet-stream, 2.3K) download From 6bffe52af59bcbf9157bd0ea8b881ce915e7a71a Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 21 Jun 2023 16:16:39 +0900 Subject: [PATCH v7 5/5] Claim SQL standard compliance for SQL/JSON features Discussion: https://postgr.es/m/d03d809c-d0fb-fd6a-1476-d6dc18ec940e@dunslane.net --- src/backend/catalog/sql_features.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt index b33065d7bf..b379c71df9 100644 --- a/src/backend/catalog/sql_features.txt +++ b/src/backend/catalog/sql_features.txt @@ -548,15 +548,15 @@ T811 Basic SQL/JSON constructor functions YES T812 SQL/JSON: JSON_OBJECTAGG YES T813 SQL/JSON: JSON_ARRAYAGG with ORDER BY YES T814 Colon in JSON_OBJECT or JSON_OBJECTAGG YES -T821 Basic SQL/JSON query operators NO +T821 Basic SQL/JSON query operators YES T822 SQL/JSON: IS JSON WITH UNIQUE KEYS predicate YES -T823 SQL/JSON: PASSING clause NO -T824 JSON_TABLE: specific PLAN clause NO -T825 SQL/JSON: ON EMPTY and ON ERROR clauses NO -T826 General value expression in ON ERROR or ON EMPTY clauses NO -T827 JSON_TABLE: sibling NESTED COLUMNS clauses NO -T828 JSON_QUERY NO -T829 JSON_QUERY: array wrapper options NO +T823 SQL/JSON: PASSING clause YES +T824 JSON_TABLE: specific PLAN clause YES +T825 SQL/JSON: ON EMPTY and ON ERROR clauses YES +T826 General value expression in ON ERROR or ON EMPTY clauses YES +T827 JSON_TABLE: sibling NESTED COLUMNS clauses YES +T828 JSON_QUERY YES +T829 JSON_QUERY: array wrapper options YES T830 Enforcing unique keys in SQL/JSON constructor functions YES T831 SQL/JSON path language: strict mode YES T832 SQL/JSON path language: item method YES @@ -565,7 +565,7 @@ T834 SQL/JSON path language: wildcard member accessor YES T835 SQL/JSON path language: filter expressions YES T836 SQL/JSON path language: starts with predicate YES T837 SQL/JSON path language: regex_like predicate YES -T838 JSON_TABLE: PLAN DEFAULT clause NO +T838 JSON_TABLE: PLAN DEFAULT clause YES T839 Formatted cast of datetimes to/from character strings NO T840 Hex integer literals in SQL/JSON path language YES T851 SQL/JSON: optional keywords for default syntax YES -- 2.35.3
v7-0003-SQL-JSON-query-functions.patch (application/octet-stream, 209.0K) download From c7ec9ee5c35b145f3d28301c5ac47c9ab08cc68a Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Fri, 16 Jun 2023 17:49:18 +0900 Subject: [PATCH v7 3/5] SQL/JSON query functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This introduces the SQL/JSON functions for querying JSON data using jsonpath expressions. The functions are: JSON_EXISTS() JSON_QUERY() JSON_VALUE() All of these functions only operate on jsonb. The workaround for now is to cast the argument to jsonb. JSON_EXISTS() tests if the jsonpath expression applied to the jsonb value yields any values. JSON_VALUE() must return a single value, and an error occurs if it tries to return multiple values. JSON_QUERY() must return a json object or array, and there are various WRAPPER options for handling scalar or multi-value results. Both these functions have options for handling EMPTY and ERROR conditions. Author: Nikita Glukhov <n.gluhov@postgrespro.ru> Author: Teodor Sigaev <teodor@sigaev.ru> Author: Oleg Bartunov <obartunov@gmail.com> Author: Alexander Korotkov <aekorotkov@gmail.com> Author: Andrew Dunstan <andrew@dunslane.net> Author: Amit Langote <amitlangote09@gmail.com> Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- doc/src/sgml/func.sgml | 147 +++ src/backend/executor/execExpr.c | 432 ++++++++ src/backend/executor/execExprInterp.c | 502 ++++++++- src/backend/jit/llvm/llvmjit_expr.c | 250 +++++ src/backend/jit/llvm/llvmjit_types.c | 5 + src/backend/nodes/makefuncs.c | 15 + src/backend/nodes/nodeFuncs.c | 183 ++++ src/backend/optimizer/path/costsize.c | 3 +- src/backend/optimizer/util/clauses.c | 19 + src/backend/parser/gram.y | 348 ++++++- src/backend/parser/parse_collate.c | 7 + src/backend/parser/parse_expr.c | 519 ++++++++- src/backend/parser/parse_target.c | 15 + src/backend/utils/adt/formatting.c | 44 + src/backend/utils/adt/jsonb.c | 62 ++ src/backend/utils/adt/jsonfuncs.c | 170 ++- src/backend/utils/adt/jsonpath.c | 255 +++++ src/backend/utils/adt/jsonpath_exec.c | 391 ++++++- src/backend/utils/adt/ruleutils.c | 136 +++ src/include/executor/execExpr.h | 172 +++ src/include/executor/executor.h | 1 + src/include/nodes/execnodes.h | 3 + src/include/nodes/makefuncs.h | 1 + src/include/nodes/parsenodes.h | 48 + src/include/nodes/primnodes.h | 109 ++ src/include/parser/kwlist.h | 11 + src/include/utils/formatting.h | 2 +- src/include/utils/jsonb.h | 3 + src/include/utils/jsonfuncs.h | 5 + src/include/utils/jsonpath.h | 27 + src/interfaces/ecpg/preproc/ecpg.trailer | 28 + src/test/regress/expected/json_sqljson.out | 18 + src/test/regress/expected/jsonb_sqljson.out | 1042 +++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/json_sqljson.sql | 11 + src/test/regress/sql/jsonb_sqljson.sql | 327 ++++++ src/tools/pgindent/typedefs.list | 14 + 37 files changed, 5234 insertions(+), 93 deletions(-) create mode 100644 src/test/regress/expected/json_sqljson.out create mode 100644 src/test/regress/expected/jsonb_sqljson.out create mode 100644 src/test/regress/sql/json_sqljson.sql create mode 100644 src/test/regress/sql/jsonb_sqljson.sql diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 9cece06c18..cb36e1463b 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16995,6 +16995,153 @@ array w/o UK? | t </tbody> </tgroup> </table> + + <para> + <xref linkend="functions-sqljson-querying"/> details the SQL/JSON + functions that can be used to query JSON data. + </para> + + <note> + <para> + SQL/JSON paths can only be applied to the <type>jsonb</type> type, so it + might be necessary to cast the <replaceable>context_item</replaceable> + argument of these functions to <type>jsonb</type>. + </para> + </note> + + <table id="functions-sqljson-querying"> + <title>SQL/JSON Query Functions</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="func_table_entry"><para role="func_signature"> + Function signature + </para> + <para> + Description + </para> + <para> + Example(s) + </para></entry> + </row> + </thead> + <tbody> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_exists</primary></indexterm> + <function>json_exists</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>TRUE</literal> | <literal>FALSE</literal> |<literal> UNKNOWN</literal> | <literal>ERROR</literal> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns true if the SQL/JSON <replaceable>path_expression</replaceable> + applied to the <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s yields any items. + The <literal>ON ERROR</literal> clause specifies what is returned if + an error occurs. Note that if the <replaceable>path_expression</replaceable> + is <literal>strict</literal>, an error is generated if it yields no items. + The default value is <literal>UNKNOWN</literal> which causes a NULL + result. + </para> + <para> + <literal>json_exists(jsonb '{"key1": [1,2,3]}', 'strict $.key1[*] ? (@ > 2)')</literal> + <returnvalue>t</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'lax $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>f</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'strict $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>ERROR: jsonpath array subscript is out of bounds</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_value</primary></indexterm> + <function>json_value</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> + <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. The extracted value must be + a single <acronym>SQL/JSON</acronym> scalar item. For results that + are objects or arrays, use the <function>json_query</function> + function instead. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + The <literal>ON EMPTY</literal> clause specifies the behavior if the + <replaceable>path_expression</replaceable> yields no value at all. + The <literal>ON ERROR</literal> clause specifies the behavior if an + error occurs as a result of <type>jsonpath</type> evaluation + (including cast to the output type) or execution of + <literal>ON EMPTY</literal> behavior (that was caused by empty result + of <type>jsonpath</type> evaluation). + </para> + <para> + <literal>json_value(jsonb '"123.45"', '$' RETURNING float)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_value(jsonb '"03:04 2015-02-01"', '$.datetime("HH24:MI&nbsp;YYYY-MM-DD")' RETURNING date)</literal> + <returnvalue>2015-02-01</returnvalue> + </para> + <para> + <literal>json_value(jsonb '[1,2]', 'strict $[*]' DEFAULT 9 ON ERROR)</literal> + <returnvalue>9</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_query</primary></indexterm> + <function>json_query</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional> + <optional> { <literal>WITHOUT</literal> | <literal>WITH</literal> { <literal>CONDITIONAL</literal> | <optional><literal>UNCONDITIONAL</literal></optional> } } <optional> <literal>ARRAY</literal> </optional> <literal>WRAPPER</literal> </optional> + <optional> { <literal>KEEP</literal> | <literal>OMIT</literal> } <literal>QUOTES</literal> <optional> <literal>ON SCALAR STRING</literal> </optional> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. + This function must return a JSON string, so if the path expression + returns multiple SQL/JSON items, you must wrap the result using the + <literal>WITH WRAPPER</literal> clause. If the wrapper is + <literal>UNCONDITIONAL</literal>, an array wrapper will always + be applied, even if the returned value is already a single JSON object + or array, but if it is <literal>CONDITIONAL</literal>, it will not be + applied to a single array or object. <literal>UNCONDITIONAL</literal> + is the default. + If the result is a scalar string, by default the value returned will have + surrounding quotes making it a valid JSON value. However, this behavior + is reversed if <literal>OMIT QUOTES</literal> is specified. + The <literal>ON ERROR</literal> and <literal>ON EMPTY</literal> + clauses have similar semantics to those clauses for + <function>json_value</function>. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + </para> + <para> + <literal>json_query(jsonb '[1,[2,3],null]', 'lax $[*][1]' WITH CONDITIONAL WRAPPER)</literal> + <returnvalue>[3]</returnvalue> + </para></entry> + </row> + </tbody> + </tgroup> + </table> + </sect2> <sect2 id="functions-sqljson-path"> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 6ca4098bef..d3d2ce00d1 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -49,6 +49,7 @@ #include "utils/builtins.h" #include "utils/datum.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -88,6 +89,16 @@ static void ExecBuildAggTransCall(ExprState *state, AggState *aggstate, FunctionCallInfo fcinfo, AggStatePerTrans pertrans, int transno, int setno, int setoff, bool ishash, bool nullcheck); +static void ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch); +static Datum GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null); +static JsonCoercionState *ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull); +static JsonItemCoercionsState *ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull); /* @@ -2411,6 +2422,14 @@ ExecInitExprRec(Expr *node, ExprState *state, break; } + case T_JsonExpr: + { + JsonExpr *jexpr = castNode(JsonExpr, node); + + ExecInitJsonExpr(jexpr, state, resv, resnull, &scratch); + break; + } + case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -4178,3 +4197,416 @@ ExecBuildParamSetEqual(TupleDesc desc, return state; } + +/* + * Push steps to evaluate a JsonExpr and its various subsidiary expressions. + */ +static void +ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch) +{ + JsonExprState *jsestate = palloc0(sizeof(JsonExprState)); + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + ListCell *argexprlc; + ListCell *argnamelc; + int skip_step_off = -1; + int passing_args_step_off = -1; + int coercion_step_off = -1; + int coercion_finish_step_off = -1; + int behavior_step_off = -1; + int onempty_expr_step_off = -1; + int onempty_jump_step_off = -1; + int onerror_expr_step_off = -1; + int onerror_jump_step_off = -1; + List *adjust_jumps = NIL; + ListCell *lc; + ExprEvalStep *as; + + jsestate->jsexpr = jexpr; + + /* + * Add steps to compute formatted_expr, pathspec, and PASSING arg + * expressions as things that must be evaluated *before* the actual JSON + * path expression. + */ + ExecInitExprRec((Expr *) jexpr->formatted_expr, state, + &pre_eval->formatted_expr.value, + &pre_eval->formatted_expr.isnull); + ExecInitExprRec((Expr *) jexpr->path_spec, state, + &pre_eval->pathspec.value, + &pre_eval->pathspec.isnull); + + /* + * Before pushing steps for PASSING args, push a step to decide whether to + * skip evaluating the args and the JSON path expression depending on + * whether either of formatted_expr and pathspec is NULL; see + * ExecEvalJsonExprSkip(). + */ + scratch->opcode = EEOP_JSONEXPR_SKIP; + scratch->d.jsonexpr_skip.jsestate = jsestate; + skip_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* PASSING args. */ + jsestate->pre_eval.args = NIL; + passing_args_step_off = state->steps_len; + forboth(argexprlc, jexpr->passing_values, + argnamelc, jexpr->passing_names) + { + Expr *argexpr = (Expr *) lfirst(argexprlc); + String *argname = lfirst_node(String, argnamelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(argname->sval); + var->typid = exprType((Node *) argexpr); + var->typmod = exprTypmod((Node *) argexpr); + + ExecInitExprRec((Expr *) argexpr, state, &var->value, &var->isnull); + + pre_eval->args = lappend(pre_eval->args, var); + } + + /* + * Step for the actual JSON path evaluation; see ExecEvalJson() and + * ExecEvalJsonExpr(). + */ + scratch->opcode = EEOP_JSONEXPR_PATH; + scratch->d.jsonexpr.jsestate = jsestate; + ExprEvalPushStep(state, scratch); + + /* + * Push steps to control the evaluation of expressions based on the result + * of JSON path evaluation. + */ + + /* + * Step to handle ON ERROR and ON EMPTY behavior; see + * ExecEvalJsonExprBehavior(). + */ + scratch->opcode = EEOP_JSONEXPR_BEHAVIOR; + scratch->d.jsonexpr_behavior.jsestate = jsestate; + behavior_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Step(s) to evaluate ON EMPTY expression */ + onempty_expr_step_off = state->steps_len; + if (jexpr->on_empty && + jexpr->on_empty->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_empty->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_empty->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_empty, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onempty_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + } + } + + /* Step(s) to evaluate ON ERROR expression */ + onerror_expr_step_off = state->steps_len; + if (jexpr->on_error && + jexpr->on_error->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_error->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_error->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_error, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onerror_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set later */ + ExprEvalPushStep(state, scratch); + } + } + + /* + * Step to handle applying coercion to the JSON item returned by + * EEOP_JSONEXPR_PATH or to the ON EMPTY/ERROR expression as + * EEOP_JSONEXPR_BEHAVIOR decides; see ExecEvalJsonExprCoercion(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Initialize coercion expression(s). */ + if (jexpr->result_coercion) + { + jsestate->result_jcstate = + ExecInitJsonCoercion(scratch, state, jexpr->result_coercion, + resv, resnull); + /* See the comment above. */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* computed later */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + if (jexpr->coercions) + { + /* + * ExecPrepareJsonItemCoercion() called by ExecEvalJsonExpr() chooses + * one for a given JSON item returned by JsonPathValue(). + */ + jsestate->item_jcstates = + ExecInitJsonItemCoercions(scratch, state, jexpr->coercions, + resv, resnull); + } + + /* + * And a step to clean up after the coercion step; see + * ExecEvalJsonExprCoercionFinish(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION_FINISH; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_finish_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* + * Adjust jump target addresses in various post-eval steps now that we + * have all the steps in place. + */ + + /* EEOP_JSONEXPR_SKIP */ + Assert(skip_step_off >= 0); + as = &state->steps[skip_step_off]; + as->d.jsonexpr_skip.jump_coercion = coercion_step_off; + as->d.jsonexpr_skip.jump_passing_args = passing_args_step_off; + + /* EEOP_JSONEXPR_BEHAVIOR */ + Assert(behavior_step_off >= 0); + as = &state->steps[behavior_step_off]; + as->d.jsonexpr_behavior.jump_onerror_expr = onerror_expr_step_off; + as->d.jsonexpr_behavior.jump_onempty_expr = onempty_expr_step_off; + as->d.jsonexpr_behavior.jump_coercion = coercion_step_off; + as->d.jsonexpr_behavior.jump_skip_coercion = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION */ + Assert(coercion_step_off >= 0); + as = &state->steps[coercion_step_off]; + as->d.jsonexpr_coercion.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION_FINISH */ + Assert(coercion_finish_step_off >= 0); + as = &state->steps[coercion_finish_step_off]; + as->d.jsonexpr_coercion_finish.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion_finish.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JUMP steps */ + + /* + * Ones after ON EMPTY and ON ERROR non-default expressions should jump to + * the coercion step. + */ + if (onempty_jump_step_off >= 0) + { + as = &state->steps[onempty_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + if (onerror_jump_step_off >= 0) + { + as = &state->steps[onerror_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + + /* The rest should jump to the end. */ + foreach(lc, adjust_jumps) + { + as = &state->steps[lfirst_int(lc)]; + as->d.jump.jumpdone = state->steps_len; + } + + /* + * Set RETURNING type's input function used by ExecEvalJsonExprCoercion(). + */ + if (jexpr->omit_quotes || + (jexpr->result_coercion && jexpr->result_coercion->via_io)) + { + Oid typinput; + FmgrInfo *finfo; + + /* lookup the result type's input function */ + getTypeInputInfo(jexpr->returning->typid, &typinput, + &jsestate->input.typioparam); + finfo = palloc0(sizeof(FmgrInfo)); + fmgr_info(typinput, finfo); + jsestate->input.finfo = finfo; + } +} + +/* + * Evaluate a JSON error/empty behavior result. + */ +static Datum +GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null) +{ + *is_null = false; + + switch (behavior->btype) + { + case JSON_BEHAVIOR_EMPTY_ARRAY: + return JsonbPGetDatum(JsonbMakeEmptyArray()); + + case JSON_BEHAVIOR_EMPTY_OBJECT: + return JsonbPGetDatum(JsonbMakeEmptyObject()); + + case JSON_BEHAVIOR_TRUE: + return BoolGetDatum(true); + + case JSON_BEHAVIOR_FALSE: + return BoolGetDatum(false); + + case JSON_BEHAVIOR_NULL: + case JSON_BEHAVIOR_UNKNOWN: + case JSON_BEHAVIOR_EMPTY: + *is_null = true; + return (Datum) 0; + + case JSON_BEHAVIOR_DEFAULT: + /* Always handled in the caller. */ + Assert(false); + return (Datum) 0; + + default: + elog(ERROR, "unrecognized SQL/JSON behavior %d", behavior->btype); + return (Datum) 0; + } +} + +/* Initialize one JsonCoercion for execution. */ +static JsonCoercionState * +ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull) +{ + JsonCoercionState *jcstate = palloc0(sizeof(JsonCoercionState)); + + jcstate->coercion = coercion; + if (coercion && coercion->expr) + { + Datum *save_innermost_caseval; + bool *save_innermost_casenull; + + jcstate->jump_eval_expr = state->steps_len; + + /* Push step(s) to compute cstate->coercion. */ + save_innermost_caseval = state->innermost_caseval; + save_innermost_casenull = state->innermost_casenull; + + state->innermost_caseval = resv; + state->innermost_casenull = resnull; + + ExecInitExprRec((Expr *) coercion->expr, state, resv, resnull); + + state->innermost_caseval = save_innermost_caseval; + state->innermost_casenull = save_innermost_casenull; + } + else + jcstate->jump_eval_expr = -1; + + return jcstate; +} + +/* + * Push steps to evaluate coercions from a given JsonItemCoercions, which + * contains all possible coercions that may need to be applied to JSON + * items coming from evaluating the parent JsonExpr. + */ +static JsonItemCoercionsState * +ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull) +{ + JsonCoercion **coercion; + JsonItemCoercionsState *item_jcstates = palloc0(sizeof(JsonItemCoercionsState)); + JsonCoercionState **item_jcstate; + ExprEvalStep *as; + List *adjust_jumps = NIL; + ListCell *lc; + + /* Push the steps of individual coercions. */ + for (coercion = &coercions->null, + item_jcstate = &item_jcstates->null; + coercion <= &coercions->composite; + coercion++, item_jcstate++) + { + *item_jcstate = ExecInitJsonCoercion(scratch, state, *coercion, + resv, resnull); + + /* Emit JUMP step to skip past other coercions' steps. */ + scratch->opcode = EEOP_JUMP; + + /* + * Remember JUMP step address to set the actual jump target address + * below. + */ + adjust_jumps = lappend_int(adjust_jumps, state->steps_len); + ExprEvalPushStep(state, scratch); + } + + foreach(lc, adjust_jumps) + { + int jump_step_id = lfirst_int(lc); + + as = &state->steps[jump_step_id]; + as->d.jump.jumpdone = state->steps_len; + } + + return item_jcstates; +} diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 76e59691e5..4c97e714ea 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -74,6 +74,7 @@ #include "utils/json.h" #include "utils/jsonb.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/timestamp.h" @@ -152,6 +153,9 @@ static TupleDesc get_cached_rowtype(Oid type_id, int32 typmod, bool *changed); static void ExecEvalRowNullInt(ExprState *state, ExprEvalStep *op, ExprContext *econtext, bool checkisnull); +static Datum ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate); /* fast-path evaluation functions */ static Datum ExecJustInnerVar(ExprState *state, ExprContext *econtext, bool *isnull); @@ -480,6 +484,11 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_XMLEXPR, &&CASE_EEOP_JSON_CONSTRUCTOR, &&CASE_EEOP_IS_JSON, + &&CASE_EEOP_JSONEXPR_SKIP, + &&CASE_EEOP_JSONEXPR_PATH, + &&CASE_EEOP_JSONEXPR_BEHAVIOR, + &&CASE_EEOP_JSONEXPR_COERCION, + &&CASE_EEOP_JSONEXPR_COERCION_FINISH, &&CASE_EEOP_AGGREF, &&CASE_EEOP_GROUPING_FUNC, &&CASE_EEOP_WINDOW_FUNC, @@ -1186,8 +1195,14 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) /* second and third arguments are already set up */ fcinfo_in->isnull = false; + + /* Pass down soft-error capture node if any. */ + fcinfo_in->context = state->escontext; + d = FunctionCallInvoke(fcinfo_in); *op->resvalue = d; + if (SOFT_ERROR_OCCURRED(state->escontext)) + *op->resnull = true; /* Should get null result if and only if str is NULL */ if (str == NULL) @@ -1195,7 +1210,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) Assert(*op->resnull); Assert(fcinfo_in->isnull); } - else + else if (!SOFT_ERROR_OCCURRED(state->escontext)) { Assert(!*op->resnull); Assert(!fcinfo_in->isnull); @@ -1543,6 +1558,38 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_JSONEXPR_PATH) + { + /* too complex for an inline implementation */ + ExecEvalJsonExpr(state, op, econtext); + EEO_NEXT(); + } + + EEO_CASE(EEOP_JSONEXPR_SKIP) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprSkip(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_BEHAVIOR) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprBehavior(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercion(state, op, econtext, + *op->resvalue, *op->resnull)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION_FINISH) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercionFinish(state, op)); + } + EEO_CASE(EEOP_AGGREF) { /* @@ -3745,7 +3792,7 @@ ExecEvalConstraintCheck(ExprState *state, ExprEvalStep *op) { if (!*op->d.domaincheck.checknull && !DatumGetBool(*op->d.domaincheck.checkvalue)) - ereport(ERROR, + errsave(state->escontext, (errcode(ERRCODE_CHECK_VIOLATION), errmsg("value for domain %s violates check constraint \"%s\"", format_type_be(op->d.domaincheck.resulttype), @@ -4138,6 +4185,457 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op) *op->resvalue = BoolGetDatum(res); } +/* + * Evaluate given JsonExpr by performing the specified JSON operation. + * + * This also populates the JsonExprPostEvalState with the information needed + * by the subsequent steps that handle the specified JsonBehavior. + */ +void +ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) +{ + JsonExprState *jsestate = op->d.jsonexpr.jsestate; + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonExpr *jexpr = jsestate->jsexpr; + Datum item; + Datum res = (Datum) 0; + bool resnull = true; + JsonPath *path; + bool *error; + bool *empty; + + item = pre_eval->formatted_expr.value; + path = DatumGetJsonPathP(pre_eval->pathspec.value); + + /* Reset JsonExprPostEvalState for this evaluation. */ + memset(post_eval, 0, sizeof(*post_eval)); + + /* Respect ON ERROR behavior during path evaluation. */ + jsestate->throw_error = (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR); + + /* + * Be sure to save the error (if needed) and empty statuses for the + * EEOP_JSONEXPR_BEHAVIOR step to peruse. + */ + error = !jsestate->throw_error ? &post_eval->error : NULL; + empty = &post_eval->empty; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + res = JsonPathQuery(item, path, jexpr->wrapper, empty, error, + pre_eval->args); + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + resnull = !DatumGetPointer(res); + break; + + case JSON_VALUE_OP: + { + JsonbValue *jbv = JsonPathValue(item, path, empty, error, + pre_eval->args); + + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + if (!jbv) /* NULL or empty */ + { + resnull = true; + break; + } + + Assert(!*empty); + + resnull = false; + + /* coerce scalar item to the output type */ + if (jexpr->returning->typid == JSONOID || + jexpr->returning->typid == JSONBOID) + { + /* Use result coercion from json[b] to the output type */ + res = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + break; + } + + /* + * Error out if no cast exists to coerce SQL/JSON item to the + * the output type. + */ + Assert(post_eval->item_jcstate == NULL); + res = ExecPrepareJsonItemCoercion(jbv, + jsestate->item_jcstates, + &post_eval->item_jcstate); + if (post_eval->item_jcstate && + post_eval->item_jcstate->coercion && + (post_eval->item_jcstate->coercion->via_io || + post_eval->item_jcstate->coercion->via_populate)) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * Coercion via I/O means here that the cast to the target + * type simply does not exist. + */ + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE), + errmsg("SQL/JSON item cannot be cast to target type"))); + } + break; + } + + case JSON_EXISTS_OP: + { + bool exists = JsonPathExists(item, path, + pre_eval->args, + error); + + resnull = error && *error; + res = BoolGetDatum(exists); + break; + } + + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * If the ON EMPTY behavior is to cause an error, do so here. Other + * behaviors will be handled by the caller. + */ + if (*empty) + { + Assert(jexpr->on_empty); /* it is not JSON_EXISTS */ + + if (jexpr->on_empty->btype == JSON_BEHAVIOR_ERROR) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + ereport(ERROR, + (errcode(ERRCODE_NO_SQL_JSON_ITEM), + errmsg("no SQL/JSON item"))); + } + } + + *op->resvalue = res; + *op->resnull = resnull; +} + +/* + * Skip calling ExecEvalJson() on the given JsonExpr? + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_skip.jsestate; + + /* + * Skip if either of the input expressions has turned out to be NULL, + * though do execute domain checks for NULLs, which are handled by the + * coercion step. + */ + if (jsestate->pre_eval.formatted_expr.isnull || + jsestate->pre_eval.pathspec.isnull) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_skip.jump_coercion; + } + + /* + * Go evaluate the PASSING args if any and subsequently JSON path itself. + */ + return op->d.jsonexpr_skip.jump_passing_args; +} + +/* + * Returns the step address to perform the JsonBehavior applicable to + * the JSON item that resulted from evaluating the given JsonExpr. + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_behavior.jsestate; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonBehavior *behavior = NULL; + int jump_to = -1; + + if (post_eval->error || post_eval->coercion_error) + { + behavior = jsestate->jsexpr->on_error; + jump_to = op->d.jsonexpr_behavior.jump_onerror_expr; + } + else if (post_eval->empty) + { + behavior = jsestate->jsexpr->on_empty; + jump_to = op->d.jsonexpr_behavior.jump_onempty_expr; + } + else if (!post_eval->coercion_done) + { + /* + * If no error or the JSON item is not empty, directly go to the + * coercion step to coerce the item as is. + */ + return op->d.jsonexpr_behavior.jump_coercion; + } + + Assert(behavior); + + /* + * Set up for coercion step that will run to coerce a non-default behavior + * value. It should use result_coercion, if any. Errors that may occur + * should be thrown for JSON ops other than JSON_VALUE_OP. + */ + if (behavior->btype != JSON_BEHAVIOR_DEFAULT) + { + post_eval->item_jcstate = NULL; + jsestate->throw_error = (jsestate->jsexpr->op != JSON_VALUE_OP); + } + + Assert(jump_to >= 0); + return jump_to; +} + +/* + * Evaluate or return the step address to evaluate a coercion of a JSON item + * to the target type. The former if the coercion must be done right away by + * calling its input function or by calling json_populate_type(). + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull) +{ + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonExpr *jexpr = jsestate->jsexpr; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonCoercionState *item_jcstate = post_eval->item_jcstate; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + + if (item_jcstate == NULL && + jsestate->jsexpr->op != JSON_EXISTS_OP) + { + ErrorSaveContext escontext = {T_ErrorSaveContext}; + Node *escontext_p; + JsonCoercion *coercion; + Jsonb *jb; + + escontext_p = !jsestate->throw_error ? (Node *) &escontext : NULL; + coercion = result_jcstate ? result_jcstate->coercion : NULL; + jb = resnull ? NULL : DatumGetJsonbP(res); + + if ((coercion && coercion->via_io) || + (jexpr->omit_quotes && !resnull && + JB_ROOT_IS_SCALAR(jb))) + { + /* strip quotes and call typinput function */ + char *str = resnull ? NULL : JsonbUnquote(jb); + bool type_is_domain; + + type_is_domain = (getBaseType(jexpr->returning->typid) != + jexpr->returning->typid); + + /* + * Catch errors only if the type is not a domain, because errors + * caused by a domain's constraint failure must be thrown right + * away. + */ + if (!InputFunctionCallSafe(jsestate->input.finfo, str, + jsestate->input.typioparam, + jexpr->returning->typmod, + !type_is_domain ? escontext_p : NULL, + op->resvalue)) + { + post_eval->error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + else if (coercion && coercion->via_populate) + { + *op->resvalue = json_populate_type(res, JSONBOID, + jexpr->returning->typid, + jexpr->returning->typmod, + &post_eval->cache, + econtext->ecxt_per_query_memory, + op->resnull, + escontext_p); + if (SOFT_ERROR_OCCURRED(escontext_p)) + { + post_eval->error = true; + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + } + + if (!jsestate->throw_error) + { + post_eval->escontext.type = T_ErrorSaveContext; + state->escontext = (Node *) &post_eval->escontext; + } + + if (item_jcstate && item_jcstate->jump_eval_expr >= 0) + return item_jcstate->jump_eval_expr; + else if (result_jcstate && result_jcstate->jump_eval_expr >= 0) + return result_jcstate->jump_eval_expr; + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; +} + +/* + * Checks if the coercion evaluation led to an error. If an error did occur, + * this returns the address of the step that handles the error, otherwise + * the step after the coercion step, which finishes the JsonExpr processing. + */ +int +ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op) +{ + JsonExprPostEvalState *post_eval = + &op->d.jsonexpr_coercion_finish.jsestate->post_eval; + + if (SOFT_ERROR_OCCURRED(state->escontext)) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + post_eval->coercion_error = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_error; + } + + /* Reset. */ + state->escontext = NULL; + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_done; +} + +/* + * Prepare SQL/JSON item coercion to the output type. Returned a datum of the + * corresponding SQL type and a pointer to the coercion state. + */ +static Datum +ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate) +{ + JsonCoercionState *item_jcstate; + Datum res; + JsonbValue buf; + + if (item->type == jbvBinary && + JsonContainerIsScalar(item->val.binary.data)) + { + bool res PG_USED_FOR_ASSERTS_ONLY; + + res = JsonbExtractScalar(item->val.binary.data, &buf); + item = &buf; + Assert(res); + } + + /* get coercion state reference and datum of the corresponding SQL type */ + switch (item->type) + { + case jbvNull: + item_jcstate = item_jcstates->null; + res = (Datum) 0; + break; + + case jbvString: + item_jcstate = item_jcstates->string; + res = PointerGetDatum(cstring_to_text_with_len(item->val.string.val, + item->val.string.len)); + break; + + case jbvNumeric: + item_jcstate = item_jcstates->numeric; + res = NumericGetDatum(item->val.numeric); + break; + + case jbvBool: + item_jcstate = item_jcstates->boolean; + res = BoolGetDatum(item->val.boolean); + break; + + case jbvDatetime: + res = item->val.datetime.value; + switch (item->val.datetime.typid) + { + case DATEOID: + item_jcstate = item_jcstates->date; + break; + case TIMEOID: + item_jcstate = item_jcstates->time; + break; + case TIMETZOID: + item_jcstate = item_jcstates->timetz; + break; + case TIMESTAMPOID: + item_jcstate = item_jcstates->timestamp; + break; + case TIMESTAMPTZOID: + item_jcstate = item_jcstates->timestamptz; + break; + default: + elog(ERROR, "unexpected jsonb datetime type oid %u", + item->val.datetime.typid); + return (Datum) 0; + } + break; + + case jbvArray: + case jbvObject: + case jbvBinary: + item_jcstate = item_jcstates->composite; + res = JsonbPGetDatum(JsonbValueToJsonb(item)); + break; + + default: + elog(ERROR, "unexpected jsonb value type %d", item->type); + return (Datum) 0; + } + + *p_item_jcstate = item_jcstate; + + return res; +} + /* * ExecEvalGroupingFunc diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 00d7b8110b..da3870cba6 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -1860,6 +1860,256 @@ llvm_compile_expr(ExprState *state) LLVMBuildBr(b, opblocks[opno + 1]); break; + case EEOP_JSONEXPR_PATH: + build_EvalXFunc(b, mod, "ExecEvalJsonExpr", + v_state, op, v_econtext); + LLVMBuildBr(b, opblocks[opno + 1]); + break; + + case EEOP_JSONEXPR_SKIP: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprSkip() to decide if JSON path + * evaluation can be skipped. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprSkip"), + params, lengthof(params), ""); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_skip.jump_coercion, which signifies + * skipping of JSON path evaluation, else to the next step + * which must point to the steps to evaluate PASSING args, + * if any, or to the JSON path evaluation. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_skip.jump_coercion), + ""), + opblocks[op->d.jsonexpr_skip.jump_coercion], + opblocks[opno + 1]); + break; + } + + case EEOP_JSONEXPR_BEHAVIOR: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + LLVMBasicBlockRef b_jump_onerror_default; + + /* + * Call ExecEvalJsonExprBehavior() to decide if ON EMPTY + * or ON ERROR behavior must be invoked depending on what + * JSON path evaluation returned. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprBehavior"), + params, lengthof(params), ""); + + b_jump_onerror_default = + l_bb_before_v(opblocks[opno + 1], + "op.%d.jsonexpr_behavior_jump_onerror_default", opno); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_behavior.jump_coercion, else to the + * next block, one that checks whether to evaluate the ON + * ERROR default expression. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_coercion), + ""), + opblocks[op->d.jsonexpr_behavior.jump_coercion], + b_jump_onerror_default); + + /* + * Block that checks whether to evaluate the ON ERROR + * default expression. + * + * Jump to evaluate the ON ERROR default expression if the + * returned address is the same as + * jsonexpr_behavior.jump_onerror_default, else jump to + * evaluate the ON EMPTY default expression. + */ + LLVMPositionBuilderAtEnd(b, b_jump_onerror_default); + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_onerror_expr), + ""), + opblocks[op->d.jsonexpr_behavior.jump_onerror_expr], + opblocks[op->d.jsonexpr_behavior.jump_onempty_expr]); + break; + } + case EEOP_JSONEXPR_COERCION: + { + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonItemCoercionsState *item_jcstates = jsestate->item_jcstates; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + LLVMValueRef v_ret; + LLVMValueRef params[5]; + + /* + * Call ExecEvalJsonExprCoercion() to evaluate appropriate + * coercion. This will return the step address to jump + * to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + params[2] = v_econtext; + params[3] = v_resvaluep; + params[4] = v_resnullp; + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercion"), + params, lengthof(params), ""); + + /* + * Jump to handle a coercion error if the returned address + * is the same as jsonexpr_coercion.jump_coercion_error, + * else to the step after coercion (coercion done!). + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + + if (item_jcstates) + { + JsonCoercionState **item_jcstate; + int n_coercions = (int) + (item_jcstates->composite - item_jcstates->null) + 1; + int i; + LLVMBasicBlockRef *b_coercions; + + + /* + * Will create a block for each coercion below to + * check whether to evaluate the coercion's expression + * if there's one or to skip to the end if not. + */ + b_coercions = palloc((n_coercions + 1) * sizeof(LLVMBasicBlockRef)); + for (i = 0; i < n_coercions + 1; i++) + b_coercions[i] = + l_bb_before_v(opblocks[opno + 1], + "op.%d.json_item_coercion.%d", + opno, i); + + /* Jump to check first coercion */ + LLVMBuildBr(b, b_coercions[0]); + + /* + * Add conditional branches for individual coercion's + * expressions + */ + for (item_jcstate = &item_jcstates->null, i = 0; + item_jcstate <= &item_jcstates->composite; + item_jcstate++, i++) + { + /* Block for this coercion */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + + /* + * Jump to evaluate the coercion's expression if + * the address returned is the same as this + * coercion's jump_eval_expr (that is, if it is + * valid), else check the next coercion's. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const((*item_jcstate)->jump_eval_expr), + ""), + (*item_jcstate)->jump_eval_expr >= 0 ? + opblocks[(*item_jcstate)->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + b_coercions[i + 1]); + } + + /* + * A placeholder block that the last coercion's block + * might jump to, which unconditionally jumps to end + * of coercions. + */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + LLVMBuildBr(b, opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + + /* + * Jump to evaluate the result_coercion's expression if + * none of the above coercions matched, that is, if + * there's one. + */ + if (result_jcstate) + { + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(result_jcstate->jump_eval_expr), + ""), + result_jcstate->jump_eval_expr >= 0 ? + opblocks[result_jcstate->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + break; + } + + case EEOP_JSONEXPR_COERCION_FINISH: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprCoercionFinish() to check whether + * an coercion error occurred, in which case we must jump + * to whatever step handles the error. This returns the + * step address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercionFinish"), + params, lengthof(params), ""); + + /* + * Jump to the step that handles coercion error if the + * returned address is the same as + * jsonexpr_coercion_finish.jump_coercion_error, else to + * jsonexpr_coercion_finish.jump_coercion_done. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion_finish.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_done]); + break; + } + case EEOP_AGGREF: { LLVMValueRef v_aggno; diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c index 41ac4c6f45..cf3ced3427 100644 --- a/src/backend/jit/llvm/llvmjit_types.c +++ b/src/backend/jit/llvm/llvmjit_types.c @@ -135,6 +135,11 @@ void *referenced_functions[] = ExecEvalXmlExpr, ExecEvalJsonConstructor, ExecEvalJsonIsPredicate, + ExecEvalJsonExprSkip, + ExecEvalJsonExprBehavior, + ExecEvalJsonExprCoercion, + ExecEvalJsonExprCoercionFinish, + ExecEvalJsonExpr, MakeExpandedObjectReadOnlyInternal, slot_getmissingattrs, slot_getsomeattrs_int, diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index c6c310d253..7bcc12b04f 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -863,6 +863,21 @@ makeJsonValueExpr(Expr *expr, JsonFormat *format) return jve; } +/* + * makeJsonBehavior - + * creates a JsonBehavior node + */ +JsonBehavior * +makeJsonBehavior(JsonBehaviorType type, Node *default_expr) +{ + JsonBehavior *behavior = makeNode(JsonBehavior); + + behavior->btype = type; + behavior->default_expr = default_expr; + + return behavior; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index dda964bd19..d31371bb4d 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -236,6 +236,12 @@ exprType(const Node *expr) case T_JsonIsPredicate: type = BOOLOID; break; + case T_JsonExpr: + type = ((const JsonExpr *) expr)->returning->typid; + break; + case T_JsonCoercion: + type = exprType(((const JsonCoercion *) expr)->expr); + break; case T_NullTest: type = BOOLOID; break; @@ -495,6 +501,10 @@ exprTypmod(const Node *expr) return exprTypmod((Node *) ((const JsonValueExpr *) expr)->formatted_expr); case T_JsonConstructorExpr: return ((const JsonConstructorExpr *) expr)->returning->typmod; + case T_JsonExpr: + return ((JsonExpr *) expr)->returning->typmod; + case T_JsonCoercion: + return exprTypmod(((const JsonCoercion *) expr)->expr); case T_CoerceToDomain: return ((const CoerceToDomain *) expr)->resulttypmod; case T_CoerceToDomainValue: @@ -971,6 +981,22 @@ exprCollation(const Node *expr) /* IS JSON's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + coll = InvalidOid; + else if (coercion->expr) + coll = exprCollation(coercion->expr); + else if (coercion->via_io || coercion->via_populate) + coll = coercion->collation; + else + coll = InvalidOid; + } + break; + case T_NullTest: /* NullTest's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ @@ -1207,6 +1233,21 @@ exprSetCollation(Node *expr, Oid collation) case T_JsonIsPredicate: Assert(!OidIsValid(collation)); /* result is always boolean */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + Assert(!OidIsValid(collation)); + else if (coercion->expr) + exprSetCollation(coercion->expr, collation); + else if (coercion->via_io || coercion->via_populate) + coercion->collation = collation; + else + Assert(!OidIsValid(collation)); + } + break; case T_NullTest: /* NullTest's result is boolean ... */ Assert(!OidIsValid(collation)); /* ... so never set a collation */ @@ -1510,6 +1551,15 @@ exprLocation(const Node *expr) case T_JsonIsPredicate: loc = ((const JsonIsPredicate *) expr)->location; break; + case T_JsonExpr: + { + const JsonExpr *jsexpr = (const JsonExpr *) expr; + + /* consider both function name and leftmost arg */ + loc = leftmostLoc(jsexpr->location, + exprLocation(jsexpr->formatted_expr)); + } + break; case T_NullTest: { const NullTest *nexpr = (const NullTest *) expr; @@ -2262,6 +2312,54 @@ expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + if (WALK(jexpr->formatted_expr)) + return true; + if (WALK(jexpr->result_coercion)) + return true; + if (WALK(jexpr->passing_values)) + return true; + /* we assume walker doesn't care about passing_names */ + if (jexpr->on_empty && + WALK(jexpr->on_empty->default_expr)) + return true; + if (WALK(jexpr->on_error->default_expr)) + return true; + if (WALK(jexpr->coercions)) + return true; + } + break; + case T_JsonCoercion: + return WALK(((JsonCoercion *) node)->expr); + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + + if (WALK(coercions->null)) + return true; + if (WALK(coercions->string)) + return true; + if (WALK(coercions->numeric)) + return true; + if (WALK(coercions->boolean)) + return true; + if (WALK(coercions->date)) + return true; + if (WALK(coercions->time)) + return true; + if (WALK(coercions->timetz)) + return true; + if (WALK(coercions->timestamp)) + return true; + if (WALK(coercions->timestamptz)) + return true; + if (WALK(coercions->composite)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: @@ -3261,6 +3359,54 @@ expression_tree_mutator_impl(Node *node, return (Node *) newnode; } + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + JsonExpr *newnode; + + FLATCOPY(newnode, jexpr, JsonExpr); + MUTATE(newnode->path_spec, jexpr->path_spec, Node *); + MUTATE(newnode->formatted_expr, jexpr->formatted_expr, Node *); + MUTATE(newnode->result_coercion, jexpr->result_coercion, JsonCoercion *); + MUTATE(newnode->passing_values, jexpr->passing_values, List *); + /* assume mutator does not care about passing_names */ + if (newnode->on_empty) + MUTATE(newnode->on_empty->default_expr, + jexpr->on_empty->default_expr, Node *); + MUTATE(newnode->on_error->default_expr, + jexpr->on_error->default_expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonCoercion: + { + JsonCoercion *coercion = (JsonCoercion *) node; + JsonCoercion *newnode; + + FLATCOPY(newnode, coercion, JsonCoercion); + MUTATE(newnode->expr, coercion->expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + JsonItemCoercions *newnode; + + FLATCOPY(newnode, coercions, JsonItemCoercions); + MUTATE(newnode->null, coercions->null, JsonCoercion *); + MUTATE(newnode->string, coercions->string, JsonCoercion *); + MUTATE(newnode->numeric, coercions->numeric, JsonCoercion *); + MUTATE(newnode->boolean, coercions->boolean, JsonCoercion *); + MUTATE(newnode->date, coercions->date, JsonCoercion *); + MUTATE(newnode->time, coercions->time, JsonCoercion *); + MUTATE(newnode->timetz, coercions->timetz, JsonCoercion *); + MUTATE(newnode->timestamp, coercions->timestamp, JsonCoercion *); + MUTATE(newnode->timestamptz, coercions->timestamptz, JsonCoercion *); + MUTATE(newnode->composite, coercions->composite, JsonCoercion *); + return (Node *) newnode; + } + break; case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -3947,6 +4093,43 @@ raw_expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonArgument: + return WALK(((JsonArgument *) node)->val); + case T_JsonCommon: + { + JsonCommon *jc = (JsonCommon *) node; + + if (WALK(jc->expr)) + return true; + if (WALK(jc->pathspec)) + return true; + if (WALK(jc->passing)) + return true; + } + break; + case T_JsonBehavior: + { + JsonBehavior *jb = (JsonBehavior *) node; + + if (jb->btype == JSON_BEHAVIOR_DEFAULT && + WALK(jb->default_expr)) + return true; + } + break; + case T_JsonFuncExpr: + { + JsonFuncExpr *jfe = (JsonFuncExpr *) node; + + if (WALK(jfe->common)) + return true; + if (jfe->output && WALK(jfe->output)) + return true; + if (WALK(jfe->on_empty)) + return true; + if (WALK(jfe->on_error)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index ef475d95a1..f58c275b4b 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -4609,7 +4609,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context) IsA(node, SQLValueFunction) || IsA(node, XmlExpr) || IsA(node, CoerceToDomain) || - IsA(node, NextValueExpr)) + IsA(node, NextValueExpr) || + IsA(node, JsonExpr)) { /* Treat all these as having cost 1 */ context->total.per_tuple += cpu_operator_cost; diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index da258968b8..e40cfab4b7 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -53,6 +53,7 @@ #include "utils/fmgroids.h" #include "utils/json.h" #include "utils/jsonb.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/syscache.h" @@ -412,6 +413,24 @@ contain_mutable_functions_walker(Node *node, void *context) /* Check all subnodes */ } + if (IsA(node, JsonExpr)) + { + JsonExpr *jexpr = castNode(JsonExpr, node); + Const *cnst; + + if (!IsA(jexpr->path_spec, Const)) + return true; + + cnst = castNode(Const, jexpr->path_spec); + + Assert(cnst->consttype == JSONPATHOID); + if (cnst->constisnull) + return false; + + return jspIsMutable(DatumGetJsonPathP(cnst->constvalue), + jexpr->passing_names, jexpr->passing_values); + } + if (IsA(node, SQLValueFunction)) { /* all variants of SQLValueFunction are stable */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 26cbebb707..0de553f5f9 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -278,6 +278,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MergeWhenClause *mergewhen; struct KeyActions *keyactions; struct KeyAction *keyaction; + JsonBehavior *jsbehavior; + JsonQuotes js_quotes; } %type <node> stmt toplevel_stmt schema_stmt routine_body_stmt @@ -650,14 +652,22 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_returning_clause_opt json_name_and_value json_aggregate_func + json_api_common_syntax + json_argument %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt + json_arguments %type <ival> json_encoding_clause_opt json_predicate_type_constraint + json_wrapper_behavior %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt +%type <jsbehavior> json_value_behavior + json_query_behavior + json_exists_behavior +%type <js_quotes> json_quotes_clause_opt /* * Non-keyword token types. These are hard-wired into the "flex" lexer. @@ -694,7 +704,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT - COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT + COMMITTED COMPRESSION CONCURRENTLY CONDITIONAL CONFIGURATION CONFLICT CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY COST CREATE CROSS CSV CUBE CURRENT_P CURRENT_CATALOG CURRENT_DATE CURRENT_ROLE CURRENT_SCHEMA @@ -705,8 +715,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P DOUBLE_P DROP - EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT - EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION + EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE + EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION EXTENSION EXTERNAL EXTRACT FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR @@ -721,10 +731,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION - JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG - JSON_SCALAR JSON_SERIALIZE + JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE - KEY KEYS + KEY KEYS KEEP LABEL LANGUAGE LARGE_P LAST_P LATERAL_P LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL @@ -738,7 +748,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC - OBJECT_P OF OFF OFFSET OIDS OLD ON ONLY OPERATOR OPTION OPTIONS OR + OBJECT_P OF OFF OFFSET OIDS OLD OMIT ON ONLY OPERATOR OPTION OPTIONS OR ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER @@ -747,7 +757,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION - QUOTE + QUOTE QUOTES RANGE READ REAL REASSIGN RECHECK RECURSIVE REF_P REFERENCES REFERENCING REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA @@ -758,7 +768,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); SEQUENCE SEQUENCES SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P - START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P + START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRING_P STRIP_P SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN @@ -766,7 +776,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); TREAT TRIGGER TRIM TRUE_P TRUNCATE TRUSTED TYPE_P TYPES_P - UESCAPE UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN + UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING @@ -15662,6 +15672,192 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_error = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->on_error = $10; + n->location = @1; + $$ = (Node *) n; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->location = @1; + $$ = (Node *) p; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + json_exists_behavior ON ERROR_P + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->on_error = $5; + p->location = @1; + $$ = (Node *) p; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->on_error = $8; + n->location = @1; + $$ = (Node *) n; + } ; @@ -16388,6 +16584,72 @@ opt_asymmetric: ASYMMETRIC ; /* SQL/JSON support */ +json_api_common_syntax: + json_value_expr ',' a_expr /* i.e. a json_path */ + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + + | json_value_expr ',' a_expr /* i.e. a json_path */ + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = $5; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = $7; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_arguments: + json_argument { $$ = list_make1($1); } + | json_arguments ',' json_argument { $$ = lappend($1, $3); } + ; + +json_argument: + json_value_expr AS ColLabel + { + JsonArgument *n = makeNode(JsonArgument); + + n->val = (JsonValueExpr *) $1; + n->name = $3; + $$ = (Node *) n; + } + ; + json_value_expr: a_expr json_format_clause_opt { @@ -16411,6 +16673,50 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; +json_query_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + | EMPTY_P ARRAY { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + | EMPTY_P OBJECT_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + /* non-standard, for Oracle compatibility only */ + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + ; + +json_exists_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | TRUE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_TRUE, NULL); } + | FALSE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_FALSE, NULL); } + | UNKNOWN { $$ = makeJsonBehavior(JSON_BEHAVIOR_UNKNOWN, NULL); } + ; + +json_value_behavior: + NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + ; + +/* ARRAY is a noise word */ +json_wrapper_behavior: + WITHOUT WRAPPER { $$ = JSW_NONE; } + | WITHOUT ARRAY WRAPPER { $$ = JSW_NONE; } + | WITH WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL ARRAY WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL WRAPPER { $$ = JSW_UNCONDITIONAL; } + | /* empty */ { $$ = JSW_NONE; } + ; + +json_quotes_clause_opt: + KEEP QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_KEEP; } + | KEEP QUOTES { $$ = JS_QUOTES_KEEP; } + | OMIT QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_OMIT; } + | OMIT QUOTES { $$ = JS_QUOTES_OMIT; } + | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } + ; + json_returning_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17013,6 +17319,7 @@ unreserved_keyword: | COMMIT | COMMITTED | COMPRESSION + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17049,10 +17356,12 @@ unreserved_keyword: | DOUBLE_P | DROP | EACH + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17102,6 +17411,7 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION + | KEEP | KEY | KEYS | LABEL @@ -17148,6 +17458,7 @@ unreserved_keyword: | OFF | OIDS | OLD + | OMIT | OPERATOR | OPTION | OPTIONS @@ -17178,6 +17489,7 @@ unreserved_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REASSIGN @@ -17237,6 +17549,7 @@ unreserved_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUPPORT @@ -17259,6 +17572,7 @@ unreserved_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNKNOWN | UNLISTEN @@ -17319,10 +17633,13 @@ col_name_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE | LEAST | NATIONAL | NCHAR @@ -17555,6 +17872,7 @@ bare_label_keyword: | COMMITTED | COMPRESSION | CONCURRENTLY + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17607,11 +17925,13 @@ bare_label_keyword: | DROP | EACH | ELSE + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | END_P | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17681,10 +18001,14 @@ bare_label_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE + | KEEP | KEY | KEYS | LABEL @@ -17745,6 +18069,7 @@ bare_label_keyword: | OFF | OIDS | OLD + | OMIT | ONLY | OPERATOR | OPTION @@ -17782,6 +18107,7 @@ bare_label_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REAL @@ -17850,6 +18176,7 @@ bare_label_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUBSTRING @@ -17884,6 +18211,7 @@ bare_label_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNIQUE | UNKNOWN diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c index 9f6afc351c..692e5d1225 100644 --- a/src/backend/parser/parse_collate.c +++ b/src/backend/parser/parse_collate.c @@ -691,6 +691,13 @@ assign_collations_walker(Node *node, assign_collations_context *context) &loccontext); } break; + case T_JsonExpr: + + /* + * Context item and PASSING arguments are already + * marked with collations in parse_expr.c. + */ + break; default: /* diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index a2644dbc77..89343e0d6a 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -90,6 +90,7 @@ static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); static Node *transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr); +static Node *transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *p); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -353,6 +354,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); break; + case T_JsonFuncExpr: + result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3225,7 +3230,7 @@ makeJsonByteaToTextConversion(Node *expr, JsonFormat *format, int location) static Node * transformJsonValueExpr(ParseState *pstate, const char *constructName, JsonValueExpr *ve, JsonFormatType default_format, - Oid targettype) + Oid targettype, bool isarg) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3262,6 +3267,35 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, else format = ve->format->format_type; } + else if (isarg) + { + /* Pass SQL/JSON item types directly without conversion to json[b]. */ + switch (exprtype) + { + case TEXTOID: + case NUMERICOID: + case BOOLOID: + case INT2OID: + case INT4OID: + case INT8OID: + case FLOAT4OID: + case FLOAT8OID: + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + return expr; + + default: + if (typcategory == TYPCATEGORY_STRING) + return expr; + /* else convert argument to json[b] type */ + break; + } + + format = default_format; + } else if (exprtype == JSONOID || exprtype == JSONBOID) format = JS_FORMAT_DEFAULT; /* do not format json[b] types */ else @@ -3273,7 +3307,8 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, Node *coerced; bool cast_is_needed = OidIsValid(targettype); - if (!cast_is_needed && + if (!isarg && + !cast_is_needed && exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), @@ -3614,7 +3649,7 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) Node *val = transformJsonValueExpr(pstate, "JSON_OBJECT()", kv->value, JS_FORMAT_DEFAULT, - InvalidOid); + InvalidOid, false); args = lappend(args, key); args = lappend(args, val); @@ -3800,7 +3835,7 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) key = transformExprRecurse(pstate, (Node *) agg->arg->key); val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", agg->arg->value, - JS_FORMAT_DEFAULT, InvalidOid); + JS_FORMAT_DEFAULT, InvalidOid, false); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3856,9 +3891,8 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) Oid aggfnoid; Oid aggtype; - arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", - agg->arg, - JS_FORMAT_DEFAULT, InvalidOid); + arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", agg->arg, + JS_FORMAT_DEFAULT, InvalidOid, false); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3905,9 +3939,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) { JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, "JSON_ARRAY()", - jsval, - JS_FORMAT_DEFAULT, - InvalidOid); + jsval, JS_FORMAT_DEFAULT, + InvalidOid, false); args = lappend(args, val); } @@ -4065,7 +4098,7 @@ transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) * function-like CASTs. */ arg = transformJsonValueExpr(pstate, "JSON()", jsexpr->expr, - JS_FORMAT_JSON, returning->typid); + JS_FORMAT_JSON, returning->typid, false); } return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, @@ -4099,9 +4132,8 @@ static Node * transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) { Node *arg = transformJsonValueExpr(pstate, "JSON_SERIALIZE()", - expr->expr, - JS_FORMAT_JSON, - InvalidOid); + expr->expr, JS_FORMAT_JSON, + InvalidOid, false); JsonReturning *returning; if (expr->output) @@ -4136,3 +4168,462 @@ transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), NULL, returning, false, false, expr->location); } + +/* + * Transform a JSON PASSING clause. + */ +static void +transformJsonPassingArgs(ParseState *pstate, char *constructName, + JsonFormatType format, List *args, + List **passing_values, List **passing_names) +{ + ListCell *lc; + + *passing_values = NIL; + *passing_names = NIL; + + foreach(lc, args) + { + JsonArgument *arg = castNode(JsonArgument, lfirst(lc)); + Node *expr = transformJsonValueExpr(pstate, constructName, + arg->val, format, + InvalidOid, true); + + assign_expr_collations(pstate, expr); + + *passing_values = lappend(*passing_values, expr); + *passing_names = lappend(*passing_names, makeString(arg->name)); + } +} + +/* + * Transform a JSON BEHAVIOR clause. + */ +static JsonBehavior * +transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior, + JsonBehaviorType default_behavior) +{ + JsonBehaviorType behavior_type = default_behavior; + Node *default_expr = NULL; + + if (behavior) + { + behavior_type = behavior->btype; + if (behavior_type == JSON_BEHAVIOR_DEFAULT) + default_expr = transformExprRecurse(pstate, behavior->default_expr); + } + return makeJsonBehavior(behavior_type, default_expr); +} + +/* + * Common code for JSON_VALUE, JSON_QUERY, JSON_EXISTS transformation + * into a JsonExpr node. + */ +static JsonExpr * +transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = makeNode(JsonExpr); + Node *pathspec; + JsonFormatType format; + char *constructName; + + if (func->common->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("JSON_TABLE path name is not allowed here"), + parser_errposition(pstate, func->location))); + + jsexpr->location = func->location; + jsexpr->op = func->op; + switch (jsexpr->op) + { + case JSON_VALUE_OP: + constructName = "JSON_VALUE()"; + break; + case JSON_QUERY_OP: + constructName = "JSON_QUERY()"; + break; + case JSON_EXISTS_OP: + constructName = "JSON_EXISTS()"; + break; + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); + break; + } + jsexpr->formatted_expr = transformJsonValueExpr(pstate, constructName, + func->common->expr, + JS_FORMAT_DEFAULT, + InvalidOid, false); + + assign_expr_collations(pstate, jsexpr->formatted_expr); + + /* format is determined by context item type */ + format = exprType(jsexpr->formatted_expr) == JSONBOID ? JS_FORMAT_JSONB : JS_FORMAT_JSON; + + jsexpr->result_coercion = NULL; + jsexpr->omit_quotes = false; + + jsexpr->format = func->common->expr->format; + + pathspec = transformExprRecurse(pstate, func->common->pathspec); + + jsexpr->path_spec = + coerce_to_target_type(pstate, pathspec, exprType(pathspec), + JSONPATHOID, -1, + COERCION_EXPLICIT, COERCE_IMPLICIT_CAST, + exprLocation(pathspec)); + if (!jsexpr->path_spec) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("JSON path expression must be type %s, not type %s", + "jsonpath", format_type_be(exprType(pathspec))), + parser_errposition(pstate, exprLocation(pathspec)))); + + /* transform and coerce to json[b] passing arguments */ + transformJsonPassingArgs(pstate, constructName, format, + func->common->passing, + &jsexpr->passing_values, + &jsexpr->passing_names); + + if (func->op != JSON_EXISTS_OP) + jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, + JSON_BEHAVIOR_NULL); + + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + func->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : + JSON_BEHAVIOR_NULL); + + return jsexpr; +} + +/* + * Assign default JSON returning type from the specified format or from + * the context item type. + */ +static void +assignDefaultJsonReturningType(Node *context_item, JsonFormat *context_format, + JsonReturning *ret) +{ + bool is_jsonb; + + ret->format = copyObject(context_format); + + if (ret->format->format_type == JS_FORMAT_DEFAULT) + is_jsonb = exprType(context_item) == JSONBOID; + else + is_jsonb = ret->format->format_type == JS_FORMAT_JSONB; + + ret->typid = is_jsonb ? JSONBOID : JSONOID; + ret->typmod = -1; +} + +/* + * Try to coerce expression to the output type or + * use json_populate_type() for composite, array and domain types or + * use coercion via I/O. + */ +static JsonCoercion * +coerceJsonExpr(ParseState *pstate, Node *expr, const JsonReturning *returning) +{ + char typtype; + JsonCoercion *coercion = makeNode(JsonCoercion); + + coercion->expr = coerceJsonFuncExpr(pstate, expr, returning, false); + + if (coercion->expr) + { + if (coercion->expr == expr) + coercion->expr = NULL; + + return coercion; + } + + typtype = get_typtype(returning->typid); + + if (returning->typid == RECORDOID || + typtype == TYPTYPE_COMPOSITE || + typtype == TYPTYPE_DOMAIN || + type_is_array(returning->typid)) + coercion->via_populate = true; + else + coercion->via_io = true; + + return coercion; +} + +/* + * Transform a JSON output clause of JSON_VALUE and JSON_QUERY. + */ +static void +transformJsonFuncExprOutput(ParseState *pstate, JsonFuncExpr *func, + JsonExpr *jsexpr) +{ + Node *expr = jsexpr->formatted_expr; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + /* JSON_VALUE returns text by default */ + if (func->op == JSON_VALUE_OP && !OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = TEXTOID; + jsexpr->returning->typmod = -1; + } + + if (OidIsValid(jsexpr->returning->typid)) + { + JsonReturning ret; + + if (func->op == JSON_VALUE_OP && + jsexpr->returning->typid != JSONOID && + jsexpr->returning->typid != JSONBOID) + { + /* Forced coercion via I/O for JSON_VALUE for non-JSON types */ + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = NULL; + jsexpr->result_coercion->via_io = true; + return; + } + + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, &ret); + + if (ret.typid != jsexpr->returning->typid || + ret.typmod != jsexpr->returning->typmod) + { + CaseTestExpr *cte = makeNode(CaseTestExpr); + + cte->typeId = exprType(expr); + cte->typeMod = exprTypmod(expr); + cte->collation = exprCollation(expr); + + Assert(cte->typeId == ret.typid); + Assert(cte->typeMod == ret.typmod); + + jsexpr->result_coercion = coerceJsonExpr(pstate, (Node *) cte, + jsexpr->returning); + } + } + else + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, + jsexpr->returning); +} + +/* + * Coerce an expression in JSON DEFAULT behavior to the target output type. + */ +static Node * +coerceDefaultJsonExpr(ParseState *pstate, JsonExpr *jsexpr, Node *defexpr) +{ + int location; + Oid exprtype; + + if (!defexpr) + return NULL; + + exprtype = exprType(defexpr); + location = exprLocation(defexpr); + + if (location < 0) + location = jsexpr->location; + + defexpr = coerce_to_target_type(pstate, + defexpr, + exprtype, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!defexpr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast DEFAULT expression type %s to %s", + format_type_be(exprtype), + format_type_be(jsexpr->returning->typid)), + parser_errposition(pstate, location))); + + return defexpr; +} + +/* + * Initialize SQL/JSON item coercion from the SQL type "typid" to the target + * "returning" type. + */ +static JsonCoercion * +initJsonItemCoercion(ParseState *pstate, Oid typid, + const JsonReturning *returning) +{ + Node *expr; + + if (typid == UNKNOWNOID) + { + expr = (Node *) makeNullConst(UNKNOWNOID, -1, InvalidOid); + } + else + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + + placeholder->typeId = typid; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + expr = (Node *) placeholder; + } + + return coerceJsonExpr(pstate, expr, returning); +} + +static void +initJsonItemCoercions(ParseState *pstate, JsonItemCoercions *coercions, + const JsonReturning *returning, Oid contextItemTypeId) +{ + struct + { + JsonCoercion **coercion; + Oid typid; + } *p, + coercionTypids[] = + { + {&coercions->null, UNKNOWNOID}, + {&coercions->string, TEXTOID}, + {&coercions->numeric, NUMERICOID}, + {&coercions->boolean, BOOLOID}, + {&coercions->date, DATEOID}, + {&coercions->time, TIMEOID}, + {&coercions->timetz, TIMETZOID}, + {&coercions->timestamp, TIMESTAMPOID}, + {&coercions->timestamptz, TIMESTAMPTZOID}, + {&coercions->composite, contextItemTypeId}, + {NULL, InvalidOid} + }; + + for (p = coercionTypids; p->coercion; p++) + *p->coercion = initJsonItemCoercion(pstate, p->typid, returning); +} + +/* + * Transform JSON_VALUE, JSON_QUERY, JSON_EXISTS functions into a JsonExpr node. + */ +static Node * +transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = transformJsonExprCommon(pstate, func); + const char *func_name = NULL; + Node *contextItemExpr = jsexpr->formatted_expr; + + /* + * Disallow FORMAT specification in the RETURNING clause of JSON_EXISTS() + * and JSON_VALUE(). + */ + if (func->output && + (func->op == JSON_VALUE_OP || func->op == JSON_EXISTS_OP)) + { + JsonFormat *format = func->output->returning->format; + + if(format->format_type != JS_FORMAT_DEFAULT || + format->encoding != JS_ENC_DEFAULT) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot specify FORMAT in RETURNING clause of %s", + func->op == JSON_VALUE_OP ? "JSON_VALUE()" : + "JSON_EXISTS()"), + parser_errposition(pstate, format->location))); + } + + switch (func->op) + { + case JSON_VALUE_OP: + func_name = "JSON_VALUE"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->coercions = makeNode(JsonItemCoercions); + initJsonItemCoercions(pstate, jsexpr->coercions, jsexpr->returning, + exprType(contextItemExpr)); + + break; + + case JSON_QUERY_OP: + func_name = "JSON_QUERY"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->wrapper = func->wrapper; + jsexpr->omit_quotes = func->omit_quotes; + + break; + + case JSON_EXISTS_OP: + func_name = "JSON_EXISTS"; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + if (!OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = BOOLOID; + jsexpr->returning->typmod = -1; + } + else if (jsexpr->returning->typid != BOOLOID) + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + int location = exprLocation((Node *) jsexpr); + + placeholder->typeId = BOOLOID; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = + coerce_to_target_type(pstate, (Node *) placeholder, BOOLOID, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!jsexpr->result_coercion->expr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(BOOLOID), + format_type_be(jsexpr->returning->typid)), + parser_coercion_errposition(pstate, location, (Node *) jsexpr))); + + if (jsexpr->result_coercion->expr == (Node *) placeholder) + jsexpr->result_coercion->expr = NULL; + } + break; + } + + if (exprType(contextItemExpr) != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("%s() is not yet implemented for the json type", func_name), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + return (Node *) jsexpr; +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 520d4f2a23..4f94fc69d6 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1979,6 +1979,21 @@ FigureColnameInternal(Node *node, char **name) /* make JSON_ARRAYAGG act like a regular function */ *name = "json_arrayagg"; return 2; + case T_JsonFuncExpr: + /* make SQL/JSON functions act like a regular function */ + switch (((JsonFuncExpr *) node)->op) + { + case JSON_QUERY_OP: + *name = "json_query"; + return 2; + case JSON_VALUE_OP: + *name = "json_value"; + return 2; + case JSON_EXISTS_OP: + *name = "json_exists"; + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index e27ea8ef97..188b5594e0 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -4426,6 +4426,50 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, } } +/* + * Parses the datetime format string in 'fmt_str' and returns true if it + * contains a timezone specifier, false if not. + */ +bool +datetime_format_has_tz(const char *fmt_str) +{ + bool incache; + int fmt_len = strlen(fmt_str); + int result; + FormatNode *format; + + if (fmt_len > DCH_CACHE_SIZE) + { + /* + * Allocate new memory if format picture is bigger than static cache + * and do not use cache (call parser always) + */ + incache = false; + + format = (FormatNode *) palloc((fmt_len + 1) * sizeof(FormatNode)); + + parse_format(format, fmt_str, DCH_keywords, + DCH_suff, DCH_index, DCH_FLAG, NULL); + } + else + { + /* + * Use cache buffers + */ + DCHCacheEntry *ent = DCH_cache_fetch(fmt_str, false); + + incache = true; + format = ent->format; + } + + result = DCH_datetime_type(format); + + if (!incache) + pfree(format); + + return result & DCH_ZONED; +} + /* * do_to_timestamp: shared code for to_timestamp and to_date * diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index 06ba409e64..d2b4da8ec8 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -2156,3 +2156,65 @@ jsonb_float8(PG_FUNCTION_ARGS) PG_RETURN_DATUM(retValue); } + +/* + * Construct an empty array jsonb. + */ +Jsonb * +JsonbMakeEmptyArray(void) +{ + JsonbValue jbv; + + jbv.type = jbvArray; + jbv.val.array.elems = NULL; + jbv.val.array.nElems = 0; + jbv.val.array.rawScalar = false; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Construct an empty object jsonb. + */ +Jsonb * +JsonbMakeEmptyObject(void) +{ + JsonbValue jbv; + + jbv.type = jbvObject; + jbv.val.object.pairs = NULL; + jbv.val.object.nPairs = 0; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Convert jsonb to a C-string stripping quotes from scalar strings. + */ +char * +JsonbUnquote(Jsonb *jb) +{ + if (JB_ROOT_IS_SCALAR(jb)) + { + JsonbValue v; + + (void) JsonbExtractScalar(&jb->root, &v); + + if (v.type == jbvString) + return pnstrdup(v.val.string.val, v.val.string.len); + else if (v.type == jbvBool) + return pstrdup(v.val.boolean ? "true" : "false"); + else if (v.type == jbvNumeric) + return DatumGetCString(DirectFunctionCall1(numeric_out, + PointerGetDatum(v.val.numeric))); + else if (v.type == jbvNull) + return pstrdup("null"); + else + { + elog(ERROR, "unrecognized jsonb value type %d", v.type); + return NULL; + } + } + else + return JsonbToCString(NULL, &jb->root, VARSIZE(jb)); +} diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index a4bfa5e404..2d5fa285fc 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -265,6 +265,7 @@ typedef struct PopulateArrayContext int *dims; /* dimensions */ int *sizes; /* current dimension counters */ int ndims; /* number of dimensions */ + Node *escontext; /* For soft-error capture */ } PopulateArrayContext; /* state for populate_array_json() */ @@ -442,12 +443,13 @@ static void JsValueToJsObject(JsValue *jsv, JsObject *jso); static Datum populate_composite(CompositeIOData *io, Oid typid, const char *colname, MemoryContext mcxt, HeapTupleHeader defaultval, JsValue *jsv, bool isnull); -static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv); +static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext); static void prepare_column_cache(ColumnIOData *column, Oid typid, int32 typmod, MemoryContext mcxt, bool need_scalar); static Datum populate_record_field(ColumnIOData *col, Oid typid, int32 typmod, const char *colname, MemoryContext mcxt, Datum defaultval, - JsValue *jsv, bool *isnull); + JsValue *jsv, bool *isnull, Node *escontext); static RecordIOData *allocate_record_info(MemoryContext mcxt, int ncolumns); static bool JsObjectGetField(JsObject *obj, char *field, JsValue *jsv); static void populate_recordset_record(PopulateRecordsetState *state, JsObject *obj); @@ -459,7 +461,8 @@ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims); static void populate_array_check_dimension(PopulateArrayContext *ctx, int ndim); static void populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv); static Datum populate_array(ArrayIOData *aio, const char *colname, - MemoryContext mcxt, JsValue *jsv); + MemoryContext mcxt, JsValue *jsv, Node *escontext, + bool *isnull); static Datum populate_domain(DomainIOData *io, Oid typid, const char *colname, MemoryContext mcxt, JsValue *jsv, bool isnull); @@ -2484,12 +2487,12 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) if (ndim <= 0) { if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the value of key \"%s\".", ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"))); } @@ -2506,13 +2509,13 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) appendStringInfo(&indices, "[%d]", ctx->sizes[i]); if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s of key \"%s\".", indices.data, ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s.", @@ -2520,7 +2523,11 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) } } -/* set the number of dimensions of the populated array when it becomes known */ +/* + * Set the number of dimensions of the populated array when it becomes known. + * + * Returns without doing anything if the input (ndims) is erratic. + */ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) { @@ -2531,6 +2538,10 @@ populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) if (ndims <= 0) populate_array_report_expected_array(ctx, ndims); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + ctx->ndims = ndims; ctx->dims = palloc(sizeof(int) * ndims); ctx->sizes = palloc0(sizeof(int) * ndims); @@ -2548,12 +2559,16 @@ populate_array_check_dimension(PopulateArrayContext *ctx, int ndim) if (ctx->dims[ndim] == -1) ctx->dims[ndim] = dim; /* assign dimension if not yet known */ else if (ctx->dims[ndim] != dim) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("malformed JSON array"), errdetail("Multidimensional arrays must have " "sub-arrays with matching dimensions."))); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + /* reset the current array dimension size counter */ ctx->sizes[ndim] = 0; @@ -2573,7 +2588,10 @@ populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv) ctx->aio->element_type, ctx->aio->element_typmod, NULL, ctx->mcxt, PointerGetDatum(NULL), - jsv, &element_isnull); + jsv, &element_isnull, ctx->escontext); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; accumArrayResult(ctx->astate, element, element_isnull, ctx->aio->element_type, ctx->acxt); @@ -2594,6 +2612,10 @@ populate_array_object_start(void *_state) else if (ndim < state->ctx->ndims) populate_array_report_expected_array(state->ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2611,6 +2633,10 @@ populate_array_array_end(void *_state) if (ndim < ctx->ndims) populate_array_check_dimension(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2686,6 +2712,10 @@ populate_array_scalar(void *_state, char *token, JsonTokenType tokentype) else if (ndim < ctx->ndims) populate_array_report_expected_array(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + if (ndim == ctx->ndims) { /* remember the scalar element token */ @@ -2715,7 +2745,13 @@ populate_array_json(PopulateArrayContext *ctx, char *json, int len) sem.array_element_end = populate_array_element_end; sem.scalar = populate_array_scalar; - pg_parse_json_or_ereport(state.lex, &sem); + if (!pg_parse_json_or_errsave(state.lex, &sem, ctx->escontext)) + { + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + pfree(state.lex); + return; + } /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2740,10 +2776,15 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ check_stack_depth(); - if (jbv->type != jbvBinary || !JsonContainerIsArray(jbc)) + if (jbv->type != jbvBinary || + !JsonContainerIsArray(jbc) || + JsonContainerIsScalar(jbc)) + { populate_array_report_expected_array(ctx, ndim - 1); - - Assert(!JsonContainerIsScalar(jbc)); + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + return; + } it = JsonbIteratorInit(jbc); @@ -2762,7 +2803,12 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ (tok == WJB_ELEM && (val.type != jbvBinary || !JsonContainerIsArray(val.val.binary.data))))) + { populate_array_assign_ndims(ctx, ndim); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + } jsv.is_json = false; jsv.val.jsonb = &val; @@ -2780,6 +2826,9 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ { /* populate child sub-array */ populate_array_dim_jsonb(ctx, &val, ndim + 1); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2797,12 +2846,18 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ Assert(tok == WJB_DONE && !it); } -/* recursively populate an array from json/jsonb */ +/* + * Recursively populate an array from json/jsonb + * + * *isnull is set to true if an error is reported during parsing. + */ static Datum populate_array(ArrayIOData *aio, const char *colname, MemoryContext mcxt, - JsValue *jsv) + JsValue *jsv, + Node *escontext, + bool *isnull) { PopulateArrayContext ctx; Datum result; @@ -2817,6 +2872,7 @@ populate_array(ArrayIOData *aio, ctx.ndims = 0; /* unknown yet */ ctx.dims = NULL; ctx.sizes = NULL; + ctx.escontext = escontext; if (jsv->is_json) populate_array_json(&ctx, jsv->val.json.str, @@ -2825,7 +2881,16 @@ populate_array(ArrayIOData *aio, else { populate_array_dim_jsonb(&ctx, jsv->val.jsonb, 1); - ctx.dims[0] = ctx.sizes[0]; + /* Nothing to do on an error. */ + if (!SOFT_ERROR_OCCURRED(ctx.escontext)) + ctx.dims[0] = ctx.sizes[0]; + } + + /* Nothing to return if an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx.escontext)) + { + *isnull = true; + return (Datum) 0; } Assert(ctx.ndims > 0); @@ -2842,6 +2907,7 @@ populate_array(ArrayIOData *aio, pfree(ctx.sizes); pfree(lbs); + *isnull = false; return result; } @@ -2957,7 +3023,8 @@ populate_composite(CompositeIOData *io, /* populate non-null scalar value from json/jsonb value */ static Datum -populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) +populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext) { Datum res; char *str = NULL; @@ -3028,7 +3095,11 @@ populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) elog(ERROR, "unrecognized jsonb type: %d", (int) jbv->type); } - res = InputFunctionCall(&io->typiofunc, str, io->typioparam, typmod); + if (!InputFunctionCallSafe(&io->typiofunc, str, io->typioparam, typmod, + escontext, &res)) + { + res = (Datum) 0; + } /* free temporary buffer */ if (str != json) @@ -3054,7 +3125,7 @@ populate_domain(DomainIOData *io, res = populate_record_field(io->base_io, io->base_typid, io->base_typmod, colname, mcxt, PointerGetDatum(NULL), - jsv, &isnull); + jsv, &isnull, NULL); Assert(!isnull); } @@ -3159,7 +3230,8 @@ populate_record_field(ColumnIOData *col, MemoryContext mcxt, Datum defaultval, JsValue *jsv, - bool *isnull) + bool *isnull, + Node *escontext) { TypeCat typcat; @@ -3192,10 +3264,12 @@ populate_record_field(ColumnIOData *col, switch (typcat) { case TYPECAT_SCALAR: - return populate_scalar(&col->scalar_io, typid, typmod, jsv); + return populate_scalar(&col->scalar_io, typid, typmod, jsv, + escontext); case TYPECAT_ARRAY: - return populate_array(&col->io.array, colname, mcxt, jsv); + return populate_array(&col->io.array, colname, mcxt, jsv, + escontext, isnull); case TYPECAT_COMPOSITE: case TYPECAT_COMPOSITE_DOMAIN: @@ -3216,6 +3290,53 @@ populate_record_field(ColumnIOData *col, } } +/* recursively populate specified type from a json/jsonb value */ +Datum +json_populate_type(Datum json_val, Oid json_type, Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext) +{ + JsValue jsv = {0}; + JsonbValue jbv; + + jsv.is_json = json_type == JSONOID; + + if (*isnull) + { + if (jsv.is_json) + jsv.val.json.str = NULL; + else + jsv.val.jsonb = NULL; + } + else if (jsv.is_json) + { + text *json = DatumGetTextPP(json_val); + + jsv.val.json.str = VARDATA_ANY(json); + jsv.val.json.len = VARSIZE_ANY_EXHDR(json); + jsv.val.json.type = JSON_TOKEN_INVALID; /* not used in + * populate_composite() */ + } + else + { + Jsonb *jsonb = DatumGetJsonbP(json_val); + + jsv.val.jsonb = &jbv; + + /* fill binary jsonb value pointing to jb */ + jbv.type = jbvBinary; + jbv.val.binary.data = &jsonb->root; + jbv.val.binary.len = VARSIZE(jsonb) - VARHDRSZ; + } + + if (!*cache) + *cache = MemoryContextAllocZero(mcxt, sizeof(ColumnIOData)); + + return populate_record_field(*cache, typid, typmod, NULL, mcxt, + PointerGetDatum(NULL), &jsv, isnull, + escontext); +} + static RecordIOData * allocate_record_info(MemoryContext mcxt, int ncolumns) { @@ -3357,7 +3478,8 @@ populate_record(TupleDesc tupdesc, mcxt, nulls[i] ? (Datum) 0 : values[i], &field, - &nulls[i]); + &nulls[i], + NULL); } res = heap_form_tuple(tupdesc, values, nulls); diff --git a/src/backend/utils/adt/jsonpath.c b/src/backend/utils/adt/jsonpath.c index 7891fde310..5194d0d91f 100644 --- a/src/backend/utils/adt/jsonpath.c +++ b/src/backend/utils/adt/jsonpath.c @@ -68,7 +68,9 @@ #include "libpq/pqformat.h" #include "nodes/miscnodes.h" #include "miscadmin.h" +#include "nodes/nodeFuncs.h" #include "utils/builtins.h" +#include "utils/formatting.h" #include "utils/json.h" #include "utils/jsonpath.h" @@ -1109,3 +1111,256 @@ jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, return true; } + +/* SQL/JSON datatype status: */ +typedef enum JsonPathDatatypeStatus +{ + jpdsNonDateTime, /* null, bool, numeric, string, array, object */ + jpdsUnknownDateTime, /* unknown datetime type */ + jpdsDateTimeZoned, /* timetz, timestamptz */ + jpdsDateTimeNonZoned /* time, timestamp, date */ +} JsonPathDatatypeStatus; + +/* Context for jspIsMutableWalker() */ +typedef struct JsonPathMutableContext +{ + List *varnames; /* list of variable names */ + List *varexprs; /* list of variable expressions */ + JsonPathDatatypeStatus current; /* status of @ item */ + bool lax; /* jsonpath is lax or strict */ + bool mutable; /* resulting mutability status */ +} JsonPathMutableContext; + +/* + * Recursive walker for jspIsMutable() + */ +static JsonPathDatatypeStatus +jspIsMutableWalker(JsonPathItem *jpi, JsonPathMutableContext *cxt) +{ + JsonPathItem next; + JsonPathDatatypeStatus status = jpdsNonDateTime; + + while (!cxt->mutable) + { + JsonPathItem arg; + JsonPathDatatypeStatus leftStatus; + JsonPathDatatypeStatus rightStatus; + + switch (jpi->type) + { + case jpiRoot: + Assert(status == jpdsNonDateTime); + break; + + case jpiCurrent: + Assert(status == jpdsNonDateTime); + status = cxt->current; + break; + + case jpiFilter: + { + JsonPathDatatypeStatus prevStatus = cxt->current; + + cxt->current = status; + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + + cxt->current = prevStatus; + break; + } + + case jpiVariable: + { + int32 len; + const char *name = jspGetString(jpi, &len); + ListCell *lc1; + ListCell *lc2; + + Assert(status == jpdsNonDateTime); + + forboth(lc1, cxt->varnames, lc2, cxt->varexprs) + { + String *varname = lfirst_node(String, lc1); + Node *varexpr = lfirst(lc2); + + if (strncmp(varname->sval, name, len)) + continue; + + switch (exprType(varexpr)) + { + case DATEOID: + case TIMEOID: + case TIMESTAMPOID: + status = jpdsDateTimeNonZoned; + break; + + case TIMETZOID: + case TIMESTAMPTZOID: + status = jpdsDateTimeZoned; + break; + + default: + status = jpdsNonDateTime; + break; + } + + break; + } + break; + } + + case jpiEqual: + case jpiNotEqual: + case jpiLess: + case jpiGreater: + case jpiLessOrEqual: + case jpiGreaterOrEqual: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + leftStatus = jspIsMutableWalker(&arg, cxt); + + jspGetRightArg(jpi, &arg); + rightStatus = jspIsMutableWalker(&arg, cxt); + + /* + * Comparison of datetime type with different timezone status + * is mutable. + */ + if (leftStatus != jpdsNonDateTime && + rightStatus != jpdsNonDateTime && + (leftStatus == jpdsUnknownDateTime || + rightStatus == jpdsUnknownDateTime || + leftStatus != rightStatus)) + cxt->mutable = true; + break; + + case jpiNot: + case jpiIsUnknown: + case jpiExists: + case jpiPlus: + case jpiMinus: + Assert(status == jpdsNonDateTime); + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiAnd: + case jpiOr: + case jpiAdd: + case jpiSub: + case jpiMul: + case jpiDiv: + case jpiMod: + case jpiStartsWith: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + jspGetRightArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiIndexArray: + for (int i = 0; i < jpi->content.array.nelems; i++) + { + JsonPathItem from; + JsonPathItem to; + + if (jspGetArraySubscript(jpi, &from, &to, i)) + jspIsMutableWalker(&to, cxt); + + jspIsMutableWalker(&from, cxt); + } + /* FALLTHROUGH */ + + case jpiAnyArray: + if (!cxt->lax) + status = jpdsNonDateTime; + break; + + case jpiAny: + if (jpi->content.anybounds.first > 0) + status = jpdsNonDateTime; + break; + + case jpiDatetime: + if (jpi->content.arg) + { + char *template; + + jspGetArg(jpi, &arg); + if (arg.type != jpiString) + { + status = jpdsNonDateTime; + break; /* there will be runtime error */ + } + + template = jspGetString(&arg, NULL); + if (datetime_format_has_tz(template)) + status = jpdsDateTimeZoned; + else + status = jpdsDateTimeNonZoned; + } + else + { + status = jpdsUnknownDateTime; + } + break; + + case jpiLikeRegex: + Assert(status == jpdsNonDateTime); + jspInitByBuffer(&arg, jpi->base, jpi->content.like_regex.expr); + jspIsMutableWalker(&arg, cxt); + break; + + /* literals */ + case jpiNull: + case jpiString: + case jpiNumeric: + case jpiBool: + /* accessors */ + case jpiKey: + case jpiAnyKey: + /* special items */ + case jpiSubscript: + case jpiLast: + /* item methods */ + case jpiType: + case jpiSize: + case jpiAbs: + case jpiFloor: + case jpiCeiling: + case jpiDouble: + case jpiKeyValue: + status = jpdsNonDateTime; + break; + } + + if (!jspGetNext(jpi, &next)) + break; + + jpi = &next; + } + + return status; +} + +/* + * Check whether jsonpath expression is immutable or not. + */ +bool +jspIsMutable(JsonPath *path, List *varnames, List *varexprs) +{ + JsonPathMutableContext cxt; + JsonPathItem jpi; + + cxt.varnames = varnames; + cxt.varexprs = varexprs; + cxt.current = jpdsNonDateTime; + cxt.lax = (path->header & JSONPATH_LAX) != 0; + cxt.mutable = false; + + jspInit(&jpi, path); + jspIsMutableWalker(&jpi, &cxt); + + return cxt.mutable; +} diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index 2d0599b4aa..eded22e194 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -87,12 +87,16 @@ typedef struct JsonBaseObjectInfo int id; } JsonBaseObjectInfo; +typedef int (*JsonPathVarCallback) (void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); + /* * Context of jsonpath execution. */ typedef struct JsonPathExecContext { - Jsonb *vars; /* variables to substitute into jsonpath */ + void *vars; /* variables to substitute into jsonpath */ + JsonPathVarCallback getVar; JsonbValue *root; /* for $ evaluation */ JsonbValue *current; /* for @ evaluation */ JsonBaseObjectInfo baseObject; /* "base object" for .keyvalue() @@ -174,7 +178,8 @@ typedef JsonPathBool (*JsonPathPredicateCallback) (JsonPathItem *jsp, void *param); typedef Numeric (*BinaryArithmFunc) (Numeric num1, Numeric num2, bool *error); -static JsonPathExecResult executeJsonPath(JsonPath *path, Jsonb *vars, +static JsonPathExecResult executeJsonPath(JsonPath *path, void *vars, + JsonPathVarCallback getVar, Jsonb *json, bool throwErrors, JsonValueList *result, bool useTz); static JsonPathExecResult executeItem(JsonPathExecContext *cxt, @@ -225,8 +230,13 @@ static JsonPathExecResult appendBoolResult(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonValueList *found, JsonPathBool res); static void getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, JsonbValue *value); +static int GetJsonPathVar(void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); static void getJsonPathVariable(JsonPathExecContext *cxt, - JsonPathItem *variable, Jsonb *vars, JsonbValue *value); + JsonPathItem *variable, JsonbValue *value); +static int getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, + int varNameLen, JsonbValue *val, + JsonbValue *baseObject); static int JsonbArraySize(JsonbValue *jb); static JsonPathBool executeComparison(JsonPathItem *cmp, JsonbValue *lv, JsonbValue *rv, void *p); @@ -284,7 +294,8 @@ jsonb_path_exists_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - res = executeJsonPath(jp, vars, jb, !silent, NULL, tz); + res = executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, NULL, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -339,7 +350,8 @@ jsonb_path_match_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -417,7 +429,8 @@ jsonb_path_query_internal(FunctionCallInfo fcinfo, bool tz) vars = PG_GETARG_JSONB_P_COPY(2); silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); funcctx->user_fctx = JsonValueListGetList(&found); @@ -464,7 +477,8 @@ jsonb_path_query_array_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_RETURN_JSONB_P(JsonbValueToJsonb(wrapItemsInArray(&found))); } @@ -495,7 +509,8 @@ jsonb_path_query_first_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); if (JsonValueListLength(&found) >= 1) PG_RETURN_JSONB_P(JsonbValueToJsonb(JsonValueListHead(&found))); @@ -537,8 +552,9 @@ jsonb_path_query_first_tz(PG_FUNCTION_ARGS) * In other case it tries to find all the satisfied result items. */ static JsonPathExecResult -executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, - JsonValueList *result, bool useTz) +executeJsonPath(JsonPath *path, void *vars, JsonPathVarCallback getVar, + Jsonb *json, bool throwErrors, JsonValueList *result, + bool useTz) { JsonPathExecContext cxt; JsonPathExecResult res; @@ -550,22 +566,16 @@ executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, if (!JsonbExtractScalar(&json->root, &jbv)) JsonbInitBinary(&jbv, json); - if (vars && !JsonContainerIsObject(&vars->root)) - { - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"vars\" argument is not an object"), - errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); - } - cxt.vars = vars; + cxt.getVar = getVar; cxt.laxMode = (path->header & JSONPATH_LAX) != 0; cxt.ignoreStructuralErrors = cxt.laxMode; cxt.root = &jbv; cxt.current = &jbv; cxt.baseObject.jbc = NULL; cxt.baseObject.id = 0; - cxt.lastGeneratedObjectId = vars ? 2 : 1; + /* 1 + number of base objects in vars */ + cxt.lastGeneratedObjectId = 1 + getVar(vars, NULL, 0, NULL, NULL); cxt.innermostArraySize = -1; cxt.throwErrors = throwErrors; cxt.useTz = useTz; @@ -2108,54 +2118,118 @@ getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, &value->val.string.len); break; case jpiVariable: - getJsonPathVariable(cxt, item, cxt->vars, value); + getJsonPathVariable(cxt, item, value); return; default: elog(ERROR, "unexpected jsonpath item type"); } } +/* + * Returns the computed value of a JSON path variable with given name. + */ +static int +GetJsonPathVar(void *cxt, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject) +{ + JsonPathVariable *var = NULL; + List *vars = cxt; + ListCell *lc; + int id = 1; + + if (!varName) + return list_length(vars); + + foreach(lc, vars) + { + JsonPathVariable *curvar = lfirst(lc); + + if (!strncmp(curvar->name, varName, varNameLen)) + { + var = curvar; + break; + } + + id++; + } + + if (!var) + return -1; + + if (var->isnull) + { + val->type = jbvNull; + return 0; + } + + JsonItemFromDatum(var->value, var->typid, var->typmod, val); + + *baseObject = *val; + return id; +} + /* * Get the value of variable passed to jsonpath executor */ static void getJsonPathVariable(JsonPathExecContext *cxt, JsonPathItem *variable, - Jsonb *vars, JsonbValue *value) + JsonbValue *value) { char *varName; int varNameLength; + JsonbValue baseObject; + int baseObjectId; + + Assert(variable->type == jpiVariable); + varName = jspGetString(variable, &varNameLength); + + if (!cxt->vars || + (baseObjectId = cxt->getVar(cxt->vars, varName, varNameLength, value, + &baseObject)) < 0) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("could not find jsonpath variable \"%s\"", + pnstrdup(varName, varNameLength)))); + + if (baseObjectId > 0) + setBaseObject(cxt, &baseObject, baseObjectId); +} + +static int +getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, int varNameLength, + JsonbValue *value, JsonbValue *baseObject) +{ + Jsonb *vars = varsJsonb; JsonbValue tmp; JsonbValue *v; - if (!vars) + if (!varName) { - value->type = jbvNull; - return; + if (vars && !JsonContainerIsObject(&vars->root)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"vars\" argument is not an object"), + errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); + } + + return vars ? 1 : 0; /* count of base objects */ } - Assert(variable->type == jpiVariable); - varName = jspGetString(variable, &varNameLength); tmp.type = jbvString; tmp.val.string.val = varName; tmp.val.string.len = varNameLength; v = findJsonbValueFromContainer(&vars->root, JB_FOBJECT, &tmp); - if (v) - { - *value = *v; - pfree(v); - } - else - { - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("could not find jsonpath variable \"%s\"", - pnstrdup(varName, varNameLength)))); - } + if (!v) + return -1; - JsonbInitBinary(&tmp, vars); - setBaseObject(cxt, &tmp, 1); + *value = *v; + pfree(v); + + JsonbInitBinary(baseObject, vars); + return 1; } /**************** Support functions for JsonPath execution *****************/ @@ -2812,3 +2886,240 @@ compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, return DatumGetInt32(DirectFunctionCall2(cmpfunc, val1, val2)); } + +/* Executor-callable JSON_EXISTS implementation */ +bool +JsonPathExists(Datum jb, JsonPath *jp, List *vars, bool *error) +{ + JsonPathExecResult res = executeJsonPath(jp, vars, GetJsonPathVar, + DatumGetJsonbP(jb), !error, NULL, + true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + *error = true; + + return res == jperOk; +} + +/* Executor-callable JSON_QUERY implementation */ +Datum +JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, bool *empty, + bool *error, List *vars) +{ + JsonbValue *first; + bool wrap; + JsonValueList found = {0}; + JsonPathExecResult res PG_USED_FOR_ASSERTS_ONLY; + int count; + + res = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + { + *error = true; + *empty = false; + return (Datum) 0; + } + + count = JsonValueListLength(&found); + + first = count ? JsonValueListHead(&found) : NULL; + + if (!first) + wrap = false; + else if (wrapper == JSW_NONE) + wrap = false; + else if (wrapper == JSW_UNCONDITIONAL) + wrap = true; + else if (wrapper == JSW_CONDITIONAL) + wrap = count > 1 || + IsAJsonbScalar(first) || + (first->type == jbvBinary && + JsonContainerIsScalar(first->val.binary.data)); + else + { + elog(ERROR, "unrecognized json wrapper %d", wrapper); + wrap = false; + } + + if (wrap) + return JsonbPGetDatum(JsonbValueToJsonb(wrapItemsInArray(&found))); + + if (count > 1) + { + if (error) + { + *error = true; + return (Datum) 0; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_QUERY should return singleton item without wrapper"), + errhint("Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array."))); + } + + if (first) + return JsonbPGetDatum(JsonbValueToJsonb(first)); + + *empty = true; + return PointerGetDatum(NULL); +} + +/* Executor-callable JSON_VALUE implementation */ +JsonbValue * +JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars) +{ + JsonbValue *res; + JsonValueList found = {0}; + JsonPathExecResult jper PG_USED_FOR_ASSERTS_ONLY; + int count; + + jper = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(jper)); + + if (error && jperIsError(jper)) + { + *error = true; + *empty = false; + return NULL; + } + + count = JsonValueListLength(&found); + + *empty = !count; + + if (*empty) + return NULL; + + if (count > 1) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + res = JsonValueListHead(&found); + + if (res->type == jbvBinary && + JsonContainerIsScalar(res->val.binary.data)) + JsonbExtractScalar(res->val.binary.data, res); + + if (!IsAJsonbScalar(res)) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_SCALAR_REQUIRED), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + if (res->type == jbvNull) + return NULL; + + return res; +} + +static void +JsonbValueInitNumericDatum(JsonbValue *jbv, Datum num) +{ + jbv->type = jbvNumeric; + jbv->val.numeric = DatumGetNumeric(num); +} + +void +JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) +{ + switch (typid) + { + case BOOLOID: + res->type = jbvBool; + res->val.boolean = DatumGetBool(val); + break; + case NUMERICOID: + JsonbValueInitNumericDatum(res, val); + break; + case INT2OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int2_numeric, val)); + break; + case INT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int4_numeric, val)); + break; + case INT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int8_numeric, val)); + break; + case FLOAT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float4_numeric, val)); + break; + case FLOAT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float8_numeric, val)); + break; + case TEXTOID: + case VARCHAROID: + res->type = jbvString; + res->val.string.val = VARDATA_ANY(val); + res->val.string.len = VARSIZE_ANY_EXHDR(val); + break; + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + res->type = jbvDatetime; + res->val.datetime.value = val; + res->val.datetime.typid = typid; + res->val.datetime.typmod = typmod; + res->val.datetime.tz = 0; + break; + case JSONBOID: + { + JsonbValue *jbv = res; + Jsonb *jb = DatumGetJsonbP(val); + + if (JsonContainerIsScalar(&jb->root)) + { + bool result PG_USED_FOR_ASSERTS_ONLY; + + result = JsonbExtractScalar(&jb->root, jbv); + Assert(result); + } + else + JsonbInitBinary(jbv, jb); + break; + } + case JSONOID: + { + text *txt = DatumGetTextP(val); + char *str = text_to_cstring(txt); + Jsonb *jb; + + jb = DatumGetJsonbP(DirectFunctionCall1(jsonb_in, + CStringGetDatum(str))); + pfree(str); + + JsonItemFromDatum(JsonbPGetDatum(jb), JSONBOID, -1, res); + break; + } + default: + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); + } +} diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d1b03d6cb2..d1ec418ccf 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -476,6 +476,8 @@ static void get_const_expr(Const *constval, deparse_context *context, int showtype); static void get_const_collation(Const *constval, deparse_context *context); static void get_json_format(JsonFormat *format, StringInfo buf); +static void get_json_returning(JsonReturning *returning, StringInfo buf, + bool json_format_by_default); static void get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, bool showimplicit); static void get_json_constructor_options(JsonConstructorExpr *ctor, @@ -518,6 +520,8 @@ static char *generate_qualified_type_name(Oid typid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); +static void get_json_path_spec(Node *path_spec, deparse_context *context, + bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8279,6 +8283,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_WindowFunc: case T_FuncExpr: case T_JsonConstructorExpr: + case T_JsonExpr: /* function-like: name(..) or name[..] */ return true; @@ -8450,6 +8455,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_GroupingFunc: /* own parentheses */ case T_WindowFunc: /* own parentheses */ case T_CaseExpr: /* other separators */ + case T_JsonExpr: /* own parentheses */ return true; default: return false; @@ -8565,6 +8571,65 @@ get_rule_expr_paren(Node *node, deparse_context *context, appendStringInfoChar(context->buf, ')'); } +static void +get_json_behavior(JsonBehavior *behavior, deparse_context *context, + const char *on) +{ + /* + * The order of array elements must correspond to the order of + * JsonBehaviorType members. + */ + const char *behavior_names[] = + { + " NULL", + " ERROR", + " EMPTY", + " TRUE", + " FALSE", + " UNKNOWN", + " EMPTY ARRAY", + " EMPTY OBJECT", + " DEFAULT " + }; + + if ((int) behavior->btype < 0 || behavior->btype >= lengthof(behavior_names)) + elog(ERROR, "invalid json behavior type: %d", behavior->btype); + + appendStringInfoString(context->buf, behavior_names[behavior->btype]); + + if (behavior->btype == JSON_BEHAVIOR_DEFAULT) + get_rule_expr(behavior->default_expr, context, false); + + appendStringInfo(context->buf, " ON %s", on); +} + +/* + * get_json_expr_options + * + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + */ +static void +get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, + JsonBehaviorType default_behavior) +{ + if (jsexpr->op == JSON_QUERY_OP) + { + if (jsexpr->wrapper == JSW_CONDITIONAL) + appendStringInfo(context->buf, " WITH CONDITIONAL WRAPPER"); + else if (jsexpr->wrapper == JSW_UNCONDITIONAL) + appendStringInfo(context->buf, " WITH UNCONDITIONAL WRAPPER"); + + if (jsexpr->omit_quotes) + appendStringInfo(context->buf, " OMIT QUOTES"); + } + + if (jsexpr->op != JSON_EXISTS_OP && + jsexpr->on_empty->btype != default_behavior) + get_json_behavior(jsexpr->on_empty, context, "EMPTY"); + + if (jsexpr->on_error->btype != default_behavior) + get_json_behavior(jsexpr->on_error, context, "ERROR"); +} /* ---------- * get_rule_expr - Parse back an expression @@ -9724,6 +9789,7 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonValueExpr: { JsonValueExpr *jve = (JsonValueExpr *) node; @@ -9773,6 +9839,63 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + appendStringInfoString(buf, "JSON_QUERY("); + break; + case JSON_VALUE_OP: + appendStringInfoString(buf, "JSON_VALUE("); + break; + case JSON_EXISTS_OP: + appendStringInfoString(buf, "JSON_EXISTS("); + break; + } + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_json_path_spec(jexpr->path_spec, context, showimplicit); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoString(buf, " PASSING "); + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + get_rule_expr((Node *) lfirst(lc2), context, showimplicit); + appendStringInfo(buf, " AS %s", + ((String *) lfirst_node(String, lc1))->sval); + } + } + + if (jexpr->op != JSON_EXISTS_OP || + jexpr->returning->typid != BOOLOID) + get_json_returning(jexpr->returning, context->buf, + jexpr->op == JSON_QUERY_OP); + + get_json_expr_options(jexpr, context, + jexpr->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : JSON_BEHAVIOR_NULL); + + appendStringInfoString(buf, ")"); + } + break; + case T_List: { char *sep; @@ -9896,6 +10019,7 @@ looks_like_function(Node *node) case T_MinMaxExpr: case T_SQLValueFunction: case T_XmlExpr: + case T_JsonExpr: /* these are all accepted by func_expr_common_subexpr */ return true; default: @@ -10755,6 +10879,18 @@ get_const_collation(Const *constval, deparse_context *context) } } +/* + * get_json_path_spec - Parse back a JSON path specification + */ +static void +get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit) +{ + if (IsA(path_spec, Const)) + get_const_expr((Const *) path_spec, context, -1); + else + get_rule_expr(path_spec, context, showimplicit); +} + /* * get_json_format - Parse back a JsonFormat node */ diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index 048573c2bc..69fb577850 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -16,12 +16,15 @@ #include "executor/nodeAgg.h" #include "nodes/execnodes.h" +#include "nodes/miscnodes.h" /* forward references to avoid circularity */ struct ExprEvalStep; struct SubscriptingRefState; struct ScalarArrayOpExprHashTable; struct JsonConstructorExprState; +struct JsonbValue; +struct JsonExprState; /* Bits in ExprState->flags (see also execnodes.h for public flag bits): */ /* expression's interpreter has been initialized */ @@ -238,6 +241,11 @@ typedef enum ExprEvalOp EEOP_XMLEXPR, EEOP_JSON_CONSTRUCTOR, EEOP_IS_JSON, + EEOP_JSONEXPR_SKIP, + EEOP_JSONEXPR_PATH, + EEOP_JSONEXPR_BEHAVIOR, + EEOP_JSONEXPR_COERCION, + EEOP_JSONEXPR_COERCION_FINISH, EEOP_AGGREF, EEOP_GROUPING_FUNC, EEOP_WINDOW_FUNC, @@ -689,6 +697,57 @@ typedef struct ExprEvalStep JsonIsPredicate *pred; /* original expression node */ } is_json; + /* for EEOP_JSONEXPR_PATH */ + struct + { + struct JsonExprState *jsestate; + } jsonexpr; + + /* for EEOP_JSONEXPR_SKIP */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprSkip() */ + int jump_coercion; + int jump_passing_args; + } jsonexpr_skip; + + /* for EEOP_JSONEXPR_BEHAVIOR */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprBehavior() */ + int jump_onerror_expr; + int jump_onempty_expr; + int jump_coercion; + int jump_skip_coercion; + } jsonexpr_behavior; + + /* for EEOP_JSONEXPR_COERCION */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion; + + /* for EEOP_JSONEXPR_COERCION_FINISH */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion_finish; } d; } ExprEvalStep; @@ -752,6 +811,111 @@ typedef struct JsonConstructorExprState int nargs; } JsonConstructorExprState; +/* + * Information computed before evaluating a JsonExpr expression. + */ +typedef struct JsonExprPreEvalState +{ + /* value/isnull for JsonExpr.formatted_expr */ + NullableDatum formatted_expr; + + /* value/isnull for JsonExpr.pathspec */ + NullableDatum pathspec; + + /* JsonPathVariable entries for JsonExpr.passing_values */ + List *args; +} JsonExprPreEvalState; + +/* + * State for a given member of JsonItemCoercions. + */ +typedef struct JsonCoercionState +{ + /* Expression used to evaluate the coercion */ + JsonCoercion *coercion; + + /* ExprEvalStep to compute this coercion's expression */ + int jump_eval_expr; +} JsonCoercionState; + +/* + * State for evaluating the coercion for a given JSON item using one of + * the following coercions. + * + * Note that while ExecInitExprRec() for JsonItemCoercions will initialize + * ExprEvalSteps for all of the members that need it, only one will get run + * during a given evaluation of the enclosing JsonExpr depending on the type + * of the result JSON item. + */ +typedef struct JsonItemCoercionsState +{ + JsonCoercionState *null; + JsonCoercionState *string; + JsonCoercionState *numeric; + JsonCoercionState *boolean; + JsonCoercionState *date; + JsonCoercionState *time; + JsonCoercionState *timetz; + JsonCoercionState *timestamp; + JsonCoercionState *timestamptz; + JsonCoercionState *composite; +} JsonItemCoercionsState; + +/* + * State for some post-JsonExpr-evaluation processing steps that gets filled + * in JsonExpr evaluation. + */ +typedef struct JsonExprPostEvalState +{ + /* Is JSON item empty? */ + bool empty; + + /* Did JSON item evaluation cause an error? */ + bool error; + + /* Cache for json_populate_type() called for coercion in some cases */ + void *cache; + + /* + * State for evaluating a JSON item coercion. Points to one of those in + * JsonExprState.coercions chosen by ExecPrepareJsonItemCoercion(). + */ + JsonCoercionState *item_jcstate; + bool coercion_error; + bool coercion_done; + + ErrorSaveContext escontext; +} JsonExprPostEvalState; + +/* EEOP_JSONEXPR state, too big to inline */ +typedef struct JsonExprState +{ + /* original expression node */ + JsonExpr *jsexpr; + + /* + * Should errors be thrown or handled softly? Different parts of + * evaluating a given JsonExpr may require different treatment depending + * on the JsonExprOp; see ExecEvalJsonExprBehavior(). + */ + bool throw_error; + + JsonExprPreEvalState pre_eval; + JsonExprPostEvalState post_eval; + + struct + { + FmgrInfo *finfo; /* typinput function for output type */ + Oid typioparam; + } input; /* I/O info for output type */ + + /* + * Either of the following two is used by ExecEvalJsonExprCoercion() to + * apply coercion to the final result if needed. + */ + JsonCoercionState *result_jcstate; + JsonItemCoercionsState *item_jcstates; +} JsonExprState; /* functions in execExpr.c */ extern void ExprEvalPushStep(ExprState *es, const ExprEvalStep *s); @@ -805,6 +969,14 @@ extern void ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op); extern void ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull); +extern int ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op); +extern void ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, + ExprContext *econtext); extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op); extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op, ExprContext *econtext); diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index c677e490d7..709ad967ba 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -17,6 +17,7 @@ #include "executor/execdesc.h" #include "fmgr.h" #include "nodes/lockoptions.h" +#include "nodes/miscnodes.h" #include "nodes/parsenodes.h" #include "utils/memutils.h" diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index cb714f4a19..584bf7001a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -129,6 +129,9 @@ typedef struct ExprState Datum *innermost_domainval; bool *innermost_domainnull; + + /* ErrorSaveContext for soft-error capture. */ + Node *escontext; } ExprState; diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 06d991b725..5e149b0266 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -111,6 +111,7 @@ extern VacuumRelation *makeVacuumRelation(RangeVar *relation, Oid oid, List *va_ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *expr, JsonFormat *format); +extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index d926713bd9..7d63a37d5f 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1727,6 +1727,12 @@ typedef enum JsonQuotes JS_QUOTES_OMIT /* OMIT QUOTES */ } JsonQuotes; +/* + * JsonPathSpec - + * representation of JSON path constant + */ +typedef char *JsonPathSpec; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1738,6 +1744,48 @@ typedef struct JsonOutput JsonReturning *returning; /* RETURNING FORMAT clause and type Oids */ } JsonOutput; +/* + * JsonArgument - + * representation of argument from JSON PASSING clause + */ +typedef struct JsonArgument +{ + NodeTag type; + JsonValueExpr *val; /* argument value expression */ + char *name; /* argument name */ +} JsonArgument; + +/* + * JsonCommon - + * representation of common syntax of functions using JSON path + */ +typedef struct JsonCommon +{ + NodeTag type; + JsonValueExpr *expr; /* context item expression */ + Node *pathspec; /* JSON path specification expression */ + char *pathname; /* path name, if any */ + List *passing; /* list of PASSING clause arguments, if any */ + int location; /* token location, or -1 if unknown */ +} JsonCommon; + +/* + * JsonFuncExpr - + * untransformed representation of JSON function expressions + */ +typedef struct JsonFuncExpr +{ + NodeTag type; + JsonExprOp op; /* expression type */ + JsonCommon *common; /* common syntax */ + JsonOutput *output; /* output clause, if specified */ + JsonBehavior *on_empty; /* ON EMPTY behavior, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + JsonWrapper wrapper; /* array wrapper behavior (JSON_QUERY only) */ + bool omit_quotes; /* omit or keep quotes? (JSON_QUERY only) */ + int location; /* token location, or -1 if unknown */ +} JsonFuncExpr; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 85e484bf43..3937114743 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1544,6 +1544,17 @@ typedef struct XmlExpr int location; } XmlExpr; +/* + * JsonExprOp - + * enumeration of JSON functions using JSON path + */ +typedef enum JsonExprOp +{ + JSON_VALUE_OP, /* JSON_VALUE() */ + JSON_QUERY_OP, /* JSON_QUERY() */ + JSON_EXISTS_OP /* JSON_EXISTS() */ +} JsonExprOp; + /* * JsonEncoding - * representation of JSON ENCODING clause @@ -1568,6 +1579,37 @@ typedef enum JsonFormatType * jsonb */ } JsonFormatType; +/* + * JsonBehaviorType - + * enumeration of behavior types used in JSON ON ... BEHAVIOR clause + * + * If enum members are reordered, get_json_behavior() from ruleutils.c + * must be updated accordingly. + */ +typedef enum JsonBehaviorType +{ + JSON_BEHAVIOR_NULL = 0, + JSON_BEHAVIOR_ERROR, + JSON_BEHAVIOR_EMPTY, + JSON_BEHAVIOR_TRUE, + JSON_BEHAVIOR_FALSE, + JSON_BEHAVIOR_UNKNOWN, + JSON_BEHAVIOR_EMPTY_ARRAY, + JSON_BEHAVIOR_EMPTY_OBJECT, + JSON_BEHAVIOR_DEFAULT +} JsonBehaviorType; + +/* + * JsonWrapper - + * representation of WRAPPER clause for JSON_QUERY() + */ +typedef enum JsonWrapper +{ + JSW_NONE, + JSW_CONDITIONAL, + JSW_UNCONDITIONAL, +} JsonWrapper; + /* * JsonFormat - * representation of JSON FORMAT clause @@ -1661,6 +1703,73 @@ typedef struct JsonIsPredicate int location; /* token location, or -1 if unknown */ } JsonIsPredicate; +/* + * JsonBehavior - + * representation of JSON ON ... BEHAVIOR clause + */ +typedef struct JsonBehavior +{ + NodeTag type; + JsonBehaviorType btype; /* behavior type */ + Node *default_expr; /* default expression, if any */ +} JsonBehavior; + +/* + * JsonCoercion - + * coercion from SQL/JSON item types to SQL types + */ +typedef struct JsonCoercion +{ + NodeTag type; + Node *expr; /* resulting expression coerced to target type */ + bool via_populate; /* coerce result using json_populate_type()? */ + bool via_io; /* coerce result using type input function? */ + Oid collation; /* collation for coercion via I/O or populate */ +} JsonCoercion; + +/* + * JsonItemCoercions - + * expressions for coercion from SQL/JSON item types directly to the + * output SQL type + */ +typedef struct JsonItemCoercions +{ + NodeTag type; + JsonCoercion *null; + JsonCoercion *string; + JsonCoercion *numeric; + JsonCoercion *boolean; + JsonCoercion *date; + JsonCoercion *time; + JsonCoercion *timetz; + JsonCoercion *timestamp; + JsonCoercion *timestamptz; + JsonCoercion *composite; /* arrays and objects */ +} JsonItemCoercions; + +/* + * JsonExpr - + * transformed representation of JSON_VALUE(), JSON_QUERY(), JSON_EXISTS() + */ +typedef struct JsonExpr +{ + Expr xpr; + JsonExprOp op; /* json function ID */ + Node *formatted_expr; /* formatted context item expression */ + JsonCoercion *result_coercion; /* resulting coercion to RETURNING type */ + JsonFormat *format; /* context item format (JSON/JSONB) */ + Node *path_spec; /* JSON path specification expression */ + List *passing_names; /* PASSING argument names */ + List *passing_values; /* PASSING argument values */ + JsonReturning *returning; /* RETURNING clause type/format info */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + JsonItemCoercions *coercions; /* coercions for JSON_VALUE */ + JsonWrapper wrapper; /* WRAPPER for JSON_QUERY */ + bool omit_quotes; /* KEEP/OMIT QUOTES for JSON_QUERY */ + int location; /* token location, or -1 if unknown */ +} JsonExpr; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 5984dcfa4b..0954d9fc7b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -93,6 +93,7 @@ PG_KEYWORD("commit", COMMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("committed", COMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("compression", COMPRESSION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("concurrently", CONCURRENTLY, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("conditional", CONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("configuration", CONFIGURATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("conflict", CONFLICT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("connection", CONNECTION, UNRESERVED_KEYWORD, BARE_LABEL) @@ -147,11 +148,13 @@ PG_KEYWORD("double", DOUBLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("drop", DROP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("each", EACH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("empty", EMPTY_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("event", EVENT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("except", EXCEPT, RESERVED_KEYWORD, AS_LABEL) @@ -233,10 +236,14 @@ PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_exists", JSON_EXISTS, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) @@ -302,6 +309,7 @@ PG_KEYWORD("off", OFF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("offset", OFFSET, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("oids", OIDS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("old", OLD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("omit", OMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("on", ON, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("only", ONLY, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("operator", OPERATOR, UNRESERVED_KEYWORD, BARE_LABEL) @@ -344,6 +352,7 @@ PG_KEYWORD("procedures", PROCEDURES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("publication", PUBLICATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("quotes", QUOTES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("range", RANGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("read", READ, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("real", REAL, COL_NAME_KEYWORD, BARE_LABEL) @@ -414,6 +423,7 @@ PG_KEYWORD("stdout", STDOUT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("storage", STORAGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("stored", STORED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("string", STRING_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("subscription", SUBSCRIPTION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("substring", SUBSTRING, COL_NAME_KEYWORD, BARE_LABEL) @@ -449,6 +459,7 @@ PG_KEYWORD("types", TYPES_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uescape", UESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unbounded", UNBOUNDED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uncommitted", UNCOMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("unconditional", UNCONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unencrypted", UNENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("union", UNION, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("unique", UNIQUE, RESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/formatting.h b/src/include/utils/formatting.h index 0cad3a2709..d4ca0f42ff 100644 --- a/src/include/utils/formatting.h +++ b/src/include/utils/formatting.h @@ -17,7 +17,6 @@ #ifndef _FORMATTING_H_ #define _FORMATTING_H_ - extern char *str_tolower(const char *buff, size_t nbytes, Oid collid); extern char *str_toupper(const char *buff, size_t nbytes, Oid collid); extern char *str_initcap(const char *buff, size_t nbytes, Oid collid); @@ -29,5 +28,6 @@ extern char *asc_initcap(const char *buff, size_t nbytes); extern Datum parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, Oid *typid, int32 *typmod, int *tz, struct Node *escontext); +extern bool datetime_format_has_tz(const char *fmt_str); #endif diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index f928e6142a..e628d4fdd0 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -422,6 +422,9 @@ extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, int estimated_len); +extern Jsonb *JsonbMakeEmptyArray(void); +extern Jsonb *JsonbMakeEmptyObject(void); +extern char *JsonbUnquote(Jsonb *jb); extern bool JsonbExtractScalar(JsonbContainer *jbc, JsonbValue *res); extern const char *JsonbTypeName(JsonbValue *val); diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index 7e9203b109..68b6c69cbb 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -15,6 +15,7 @@ #define JSONFUNCS_H #include "common/jsonapi.h" +#include "nodes/nodes.h" #include "utils/jsonb.h" /* @@ -86,5 +87,9 @@ extern Datum to_json_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid); extern Datum to_jsonb_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid); +extern Datum json_populate_type(Datum json_val, Oid json_type, + Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext); #endif diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index f0181e045f..4dae78a98c 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -16,7 +16,9 @@ #include "fmgr.h" #include "nodes/pg_list.h" +#include "nodes/primnodes.h" #include "utils/jsonb.h" +#include "utils/jsonfuncs.h" typedef struct { @@ -184,6 +186,7 @@ extern bool jspGetBool(JsonPathItem *v); extern char *jspGetString(JsonPathItem *v, int32 *len); extern bool jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, int i); +extern bool jspIsMutable(JsonPath *path, List *varnames, List *varexprs); extern const char *jspOperationName(JsonPathItemType type); @@ -261,4 +264,28 @@ extern bool jspConvertRegexFlags(uint32 xflags, int *result, struct Node *escontext); +/* + * Evaluation of jsonpath + */ + +/* External variable passed into jsonpath. */ +typedef struct JsonPathVariable +{ + char *name; + Oid typid; + int32 typmod; + Datum value; + bool isnull; +} JsonPathVariable; + +/* SQL/JSON item */ +extern void JsonItemFromDatum(Datum val, Oid typid, int32 typmod, + JsonbValue *res); + +extern bool JsonPathExists(Datum jb, JsonPath *path, List *vars, bool *error); +extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, + bool *empty, bool *error, List *vars); +extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, + bool *error, List *vars); + #endif diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer index 435c139ec2..b2aa44f36d 100644 --- a/src/interfaces/ecpg/preproc/ecpg.trailer +++ b/src/interfaces/ecpg/preproc/ecpg.trailer @@ -651,6 +651,34 @@ var_type: simple_type $$.type_index = mm_strdup("-1"); $$.type_sizeof = NULL; } + | STRING_P + { + if (INFORMIX_MODE) + { + /* In Informix mode, "string" is automatically a typedef */ + $$.type_enum = ECPGt_string; + $$.type_str = mm_strdup("char"); + $$.type_dimension = mm_strdup("-1"); + $$.type_index = mm_strdup("-1"); + $$.type_sizeof = NULL; + } + else + { + /* Otherwise, legal only if user typedef'ed it */ + struct typedefs *this = get_typedef("string", false); + + $$.type_str = (this->type->type_enum == ECPGt_varchar || this->type->type_enum == ECPGt_bytea) ? EMPTY : mm_strdup(this->name); + $$.type_enum = this->type->type_enum; + $$.type_dimension = this->type->type_dimension; + $$.type_index = this->type->type_index; + if (this->type->type_sizeof && strlen(this->type->type_sizeof) != 0) + $$.type_sizeof = this->type->type_sizeof; + else + $$.type_sizeof = cat_str(3, mm_strdup("sizeof("), mm_strdup(this->name), mm_strdup(")")); + + struct_member_list[struct_level] = ECPGstruct_member_dup(this->struct_member_list); + } + } | INTERVAL ecpg_interval { $$.type_enum = ECPGt_interval; diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out new file mode 100644 index 0000000000..8b87580752 --- /dev/null +++ b/src/test/regress/expected/json_sqljson.out @@ -0,0 +1,18 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); +ERROR: JSON_EXISTS() is not yet implemented for the json type +LINE 1: SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_VALUE +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); +ERROR: JSON_VALUE() is not yet implemented for the json type +LINE 1: SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_QUERY +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); +ERROR: JSON_QUERY() is not yet implemented for the json type +LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out new file mode 100644 index 0000000000..22f8679917 --- /dev/null +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -0,0 +1,1042 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL::jsonb, '$'); + json_exists +------------- + +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb 'null', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_EXISTS(jsonb 'null', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{}', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + json_exists +------------- + f +(1 row) + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + json_exists +------------- + t +(1 row) + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); + json_exists +------------- + 1 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); + json_exists +------------- + 0 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); + json_exists +------------- + true +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +ERROR: cannot cast type boolean to jsonb +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +ERROR: cannot cast type boolean to real +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_EXISTS() +LINE 1: ...CT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSO... + ^ +-- JSON_VALUE +SELECT JSON_VALUE(NULL::jsonb, '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$'); + json_value +------------ + true +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + json_value +------------ + t +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); + json_value +------------ + 123 +(1 row) + +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); +ERROR: SQL/JSON item cannot be cast to target type +SELECT JSON_VALUE(jsonb '1.23', '$'); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "1.23" +SELECT JSON_VALUE(jsonb '"aaa"', '$'); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); + json_value +------------ + aa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "aaa" +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); + json_value +------------ + 111 +(1 row) + +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + ?column? +------------ + 03-01-2017 +(1 row) + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '[]', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '{}', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '1', '$.a'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); + json_value +------------ + error +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 3 +(1 row) + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); + json_value +------------ + 0 +(1 row) + +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: " " +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 5 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_VALUE() +LINE 1: ...CT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSO... + ^ +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + x | y +---+---- + 0 | -2 + 1 | 2 + 2 | -1 +(3 rows) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); + json_value +------------ + (1,2) +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + json_value +------------ + (1,2) +(1 row) + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); + json_value +-------------------------- + Tue Feb 20 18:34:56 2018 +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- JSON_QUERY +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + json_query | json_query | json_query | json_query | json_query +--------------------+--------------------+--------------------+----------------------+---------------------- + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, null, "2"] | [1, null, "2"] | [1, null, "2"] | [[1, null, "2"]] | [[1, null, "2"]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] +(6 rows) + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + unspec | without | with cond | with uncond | with +--------------------+--------------------+---------------------+----------------------+---------------------- + | | | | + | | | | + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, 2, 3] | [1, 2, 3] | [1, 2, 3] | [[1, 2, 3]] | [[1, 2, 3]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] + | | [1, "2", null, [3]] | [1, "2", null, [3]] | [1, "2", null, [3]] +(9 rows) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + json_query +------------ + \x616161 +(1 row) + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTE... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTE... + ^ +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_QUERY should return singleton item without wrapper +HINT: Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array. +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); + json_query +------------ + [1, +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); +ERROR: expected JSON array +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + x | y | list +---+---+-------------- + 0 | 0 | [] + 0 | 1 | [1] + 0 | 2 | [1, 2] + 0 | 3 | [1, 2, 3] + 0 | 4 | [1, 2, 3, 4] + 1 | 0 | [] + 1 | 1 | [1] + 1 | 2 | [1, 2] + 1 | 3 | [1, 2, 3] + 1 | 4 | [1, 2, 3, 4] + 2 | 0 | [] + 2 | 1 | [] + 2 | 2 | [2] + 2 | 3 | [2, 3] + 2 | 4 | [2, 3, 4] + 3 | 0 | [] + 3 | 1 | [] + 3 | 2 | [] + 3 | 3 | [3] + 3 | 4 | [3, 4] + 4 | 0 | [] + 4 | 1 | [] + 4 | 2 | [] + 4 | 3 | [] + 4 | 4 | [4] +(25 rows) + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); + json_query +----------------------------------------------------- + (1,aaa,"[1, ""2"", {}]","{""x"": [1, ""2"", {}]}",) +(1 row) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); + unnest +------------------------ + {"a": 1, "b": ["foo"]} + {"a": 2, "c": {}} + 123 +(3 rows) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); + json_query +-------------- + {1,2,NULL,3} +(1 row) + +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); + json_query +------------ + 1 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- Test constraints +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); +\d test_jsonb_constraints + Table "public.test_jsonb_constraints" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+-------------------------------------------------------------------------------- + js | text | | | + i | integer | | | + x | jsonb | | | JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +Check constraints: + "test_jsonb_constraint1" CHECK (js IS JSON) + "test_jsonb_constraint2" CHECK (JSON_EXISTS(js::jsonb, '$."a"' PASSING i + 5 AS int, i::text AS txt, ARRAY[1, 2, 3] AS arr)) + "test_jsonb_constraint3" CHECK (JSON_VALUE(js::jsonb, '$."a"' RETURNING integer DEFAULT ('12'::text || i)::integer ON EMPTY ERROR ON ERROR) > i) + "test_jsonb_constraint4" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb) + "test_jsonb_constraint5" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C")) + "test_jsonb_constraint6" CHECK (JSON_EXISTS(js::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2) + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + check_clause +-------------------------------------------------------------------------------------------------------------------------- + ((JSON_EXISTS((js)::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2)) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C"))) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb)) + ((JSON_VALUE((js)::jsonb, '$."a"' RETURNING integer DEFAULT (('12'::text || i))::integer ON EMPTY ERROR ON ERROR) > i)) + ((js IS JSON)) + (JSON_EXISTS((js)::jsonb, '$."a"' PASSING (i + 5) AS int, (i)::text AS txt, ARRAY[1, 2, 3] AS arr)) +(6 rows) + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +WHERE constraint_name LIKE 'test_jsonb_constraint%'; +ERROR: syntax error at or near "WHERE" +LINE 1: WHERE constraint_name LIKE 'test_jsonb_constraint%'; + ^ +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint1" +DETAIL: Failing row contains (, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains (1, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('[]'); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ([], null, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ({"b": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint3" +DETAIL: Failing row contains ({"a": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint5" +DETAIL: Failing row contains ({"a": 7}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint4" +DETAIL: Failing row contains ({"a": 10}, 1, [1, 2]). +DROP TABLE test_jsonb_constraints; +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 4df9d8503b..9b0ecf049d 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -103,7 +103,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo # ---------- # Another group of parallel tests (JSON related) # ---------- -test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson +test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson json_sqljson jsonb_sqljson # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql new file mode 100644 index 0000000000..4f30fa46b9 --- /dev/null +++ b/src/test/regress/sql/json_sqljson.sql @@ -0,0 +1,11 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + +-- JSON_QUERY + +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql new file mode 100644 index 0000000000..b8a6148b7b --- /dev/null +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -0,0 +1,327 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL::jsonb, '$'); + +SELECT JSON_EXISTS(jsonb '[]', '$'); +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + +SELECT JSON_EXISTS(jsonb '1', '$'); +SELECT JSON_EXISTS(jsonb 'null', '$'); +SELECT JSON_EXISTS(jsonb '[]', '$'); + +SELECT JSON_EXISTS(jsonb '1', '$.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_EXISTS(jsonb 'null', '$.a'); +SELECT JSON_EXISTS(jsonb '[]', '$.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); +SELECT JSON_EXISTS(jsonb '{}', '$.a'); +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSON); -- RETURNING FORMAT not allowed + + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL::jsonb, '$'); + +SELECT JSON_VALUE(jsonb 'null', '$'); +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + +SELECT JSON_VALUE(jsonb 'true', '$'); +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + +SELECT JSON_VALUE(jsonb '123', '$'); +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1.23', '$'); +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '"aaa"', '$'); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); + +SELECT JSON_VALUE(jsonb '[]', '$'); +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '{}', '$'); +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1', '$.a'); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed + +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- JSON_QUERY + +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); + +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); + +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); + +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- Test constraints + +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); + +\d test_jsonb_constraints + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; +WHERE constraint_name LIKE 'test_jsonb_constraint%'; + +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +INSERT INTO test_jsonb_constraints VALUES ('[]'); +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); + +DROP TABLE test_jsonb_constraints; + +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index d251fb9a91..6e4c026492 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1254,14 +1254,24 @@ JsonArrayAgg JsonArrayConstructor JsonArrayQueryConstructor JsonBaseObjectInfo +JsonBehavior +JsonBehaviorType +JsonCoercion +JsonCoercionState +JsonCommon JsonConstructorExpr JsonConstructorExprState JsonConstructorType JsonEncoding +JsonExpr +JsonExprOp +JsonExprState JsonFormat JsonFormatType JsonHashEntry JsonIsPredicate +JsonItemCoercions +JsonItemCoercionsState JsonIterateStringValuesAction JsonKeyValue JsonLexContext @@ -1294,6 +1304,10 @@ JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback JsonPathString +JsonPathVarCallback +JsonPathVariable +JsonPathVariableEvalContext +JsonQuotes JsonReturning JsonSemAction JsonSerializeExpr -- 2.35.3
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-17 05:00  jian he <jian.universality@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: jian he @ 2023-07-17 05:00 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>; pgsql-hackers; Andrew Dunstan <andrew@dunslane.net>

hi.
seems there is no explanation about, json_api_common_syntax in
functions-json.html

I can get json_query full synopsis from functions-json.html as follows:
json_query ( context_item, path_expression [ PASSING { value AS
varname } [, ...]] [ RETURNING data_type [ FORMAT JSON [ ENCODING UTF8
] ] ] [ { WITHOUT | WITH { CONDITIONAL | [UNCONDITIONAL] } } [ ARRAY ]
WRAPPER ] [ { KEEP | OMIT } QUOTES [ ON SCALAR STRING ] ] [ { ERROR |
NULL | EMPTY { [ ARRAY ] | OBJECT } | DEFAULT expression } ON EMPTY ]
[ { ERROR | NULL | EMPTY { [ ARRAY ] | OBJECT } | DEFAULT expression }
ON ERROR ])

seems doesn't  have a full synopsis for json_table? only partial one
by  one explanation.





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-17 07:15  Erik Rijkers <er@xs4all.nl>
  parent: jian he <jian.universality@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Erik Rijkers @ 2023-07-17 07:15 UTC (permalink / raw)
  To: jian he <jian.universality@gmail.com>; Amit Langote <amitlangote09@gmail.com>; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>; pgsql-hackers; Andrew Dunstan <andrew@dunslane.net>

Op 7/17/23 om 07:00 schreef jian he:
> hi.
> seems there is no explanation about, json_api_common_syntax in
> functions-json.html
> 
> I can get json_query full synopsis from functions-json.html as follows:
> json_query ( context_item, path_expression [ PASSING { value AS
> varname } [, ...]] [ RETURNING data_type [ FORMAT JSON [ ENCODING UTF8
> ] ] ] [ { WITHOUT | WITH { CONDITIONAL | [UNCONDITIONAL] } } [ ARRAY ]
> WRAPPER ] [ { KEEP | OMIT } QUOTES [ ON SCALAR STRING ] ] [ { ERROR |
> NULL | EMPTY { [ ARRAY ] | OBJECT } | DEFAULT expression } ON EMPTY ]
> [ { ERROR | NULL | EMPTY { [ ARRAY ] | OBJECT } | DEFAULT expression }
> ON ERROR ])
> 
> seems doesn't  have a full synopsis for json_table? only partial one
> by  one explanation.
> 

FWIW, Re: json_api_common_syntax

An (old) pdf that I have (ISO/IEC TR 19075-6 First edition 2017-03)
contains the below specification.  It's probably the source of the 
particular term.  It's easy to see how it maps onto the current v7 
SQL/JSON implementation.   (I don't know if it has changed in later 
incarnations.)


------ 8< ------------
5.2  JSON API common syntax

The SQL/JSON query functions all need a path specification, the JSON 
value to be input to that path specification for querying and 
processing, and optional parameter values passed to the path 
specification. They use a common syntax:

<JSON API common syntax> ::=
   <JSON context item> <comma> <JSON path specification>
      [ AS <JSON table path name> ]
      [ <JSON passing clause> ]

<JSON context item> ::=
    <JSON value expression>

<JSON path specification> ::=
   <character string literal>

<JSON passing clause> ::=
    PASSING <JSON argument> [ { <comma> <JSON argument> } ]

<JSON argument> ::=
    <JSON value expression> AS <identifier>

------ 8< ------------

And yes, we might need a readable translation of that in the docs 
although it might be easier to just get get rid of the term 
'json_api_common_syntax'.

HTH,

Erik Rijkers





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-18 09:11  Amit Langote <amitlangote09@gmail.com>
  parent: Erik Rijkers <er@xs4all.nl>
  0 siblings, 2 replies; 300+ messages in thread

From: Amit Langote @ 2023-07-18 09:11 UTC (permalink / raw)
  To: Erik Rijkers <er@xs4all.nl>; +Cc: jian he <jian.universality@gmail.com>; Alvaro Herrera <alvherre@alvh.no-ip.org>; pgsql-hackers; Andrew Dunstan <andrew@dunslane.net>

Hi,

On Mon, Jul 17, 2023 at 4:14 PM Erik Rijkers <er@xs4all.nl> wrote:
> Op 7/17/23 om 07:00 schreef jian he:
> > hi.
> > seems there is no explanation about, json_api_common_syntax in
> > functions-json.html
> >
> > I can get json_query full synopsis from functions-json.html as follows:
> > json_query ( context_item, path_expression [ PASSING { value AS
> > varname } [, ...]] [ RETURNING data_type [ FORMAT JSON [ ENCODING UTF8
> > ] ] ] [ { WITHOUT | WITH { CONDITIONAL | [UNCONDITIONAL] } } [ ARRAY ]
> > WRAPPER ] [ { KEEP | OMIT } QUOTES [ ON SCALAR STRING ] ] [ { ERROR |
> > NULL | EMPTY { [ ARRAY ] | OBJECT } | DEFAULT expression } ON EMPTY ]
> > [ { ERROR | NULL | EMPTY { [ ARRAY ] | OBJECT } | DEFAULT expression }
> > ON ERROR ])
> >
> > seems doesn't  have a full synopsis for json_table? only partial one
> > by  one explanation.

I looked through the history of the docs portion of the patch and it
looks like the synopsis for JSON_TABLE(...) used to be there but was
taken out during one of the doc reworks [1].

I've added it back in the patch as I agree that it would help to have
it.   Though, I am not totally sure where I've put it is the right
place for it.  JSON_TABLE() is a beast that won't fit into the table
that JSON_QUERY() et al are in, so maybe that's how it will have to
be?  I have no better idea.

> FWIW, Re: json_api_common_syntax
...
> An (old) pdf that I have (ISO/IEC TR 19075-6 First edition 2017-03)
> contains the below specification.  It's probably the source of the
> particular term.  It's easy to see how it maps onto the current v7
> SQL/JSON implementation.   (I don't know if it has changed in later
> incarnations.)
>
>
> ------ 8< ------------
> 5.2  JSON API common syntax
>
> The SQL/JSON query functions all need a path specification, the JSON
> value to be input to that path specification for querying and
> processing, and optional parameter values passed to the path
> specification. They use a common syntax:
>
> <JSON API common syntax> ::=
>    <JSON context item> <comma> <JSON path specification>
>       [ AS <JSON table path name> ]
>       [ <JSON passing clause> ]
>
> <JSON context item> ::=
>     <JSON value expression>
>
> <JSON path specification> ::=
>    <character string literal>
>
> <JSON passing clause> ::=
>     PASSING <JSON argument> [ { <comma> <JSON argument> } ]
>
> <JSON argument> ::=
>     <JSON value expression> AS <identifier>
>
> ------ 8< ------------
>
> And yes, we might need a readable translation of that in the docs
> although it might be easier to just get get rid of the term
> 'json_api_common_syntax'.

I found a patch proposed by Andrew Dunstan in the v15 dev cycle to get
rid of the term in the JSON_TABLE docs that Erik seemed to agree with
[2], so I've applied it.

Attached updated patches.  In 0002, I removed the mention of the
RETURNING clause in the JSON(), JSON_SCALAR() documentation, which I
had forgotten to do in the last version which removed its support in
code.

I think 0001 looks ready to go.  Alvaro?

Also, I've been wondering if it isn't too late to apply the following
to v16 too, so as to make the code look similar in both branches:

b6e1157e7d Don't include CaseTestExpr in JsonValueExpr.formatted_expr
785480c953 Pass constructName to transformJsonValueExpr()

--
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

[1] https://www.postgresql.org/message-id/044204fa-738d-d89a-0e81-1c04696ba676%40dunslane.net
[2] https://www.postgresql.org/message-id/10c997db-9270-bdd5-04d5-0ffc1eefcdb7%40dunslane.net

Attachments: download all (5 files, .tar.gz)

v8-0005-Claim-SQL-standard-compliance-for-SQL-JSON-featur.patch (application/octet-stream, 2.3K) download From 87c95f4a7c7671420a1ea7c64c0aa1e7268b0102 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Tue, 18 Jul 2023 17:58:44 +0900 Subject: [PATCH v8 5/5] Claim SQL standard compliance for SQL/JSON features Discussion: https://postgr.es/m/d03d809c-d0fb-fd6a-1476-d6dc18ec940e@dunslane.net --- src/backend/catalog/sql_features.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt index b33065d7bf..b379c71df9 100644 --- a/src/backend/catalog/sql_features.txt +++ b/src/backend/catalog/sql_features.txt @@ -548,15 +548,15 @@ T811 Basic SQL/JSON constructor functions YES T812 SQL/JSON: JSON_OBJECTAGG YES T813 SQL/JSON: JSON_ARRAYAGG with ORDER BY YES T814 Colon in JSON_OBJECT or JSON_OBJECTAGG YES -T821 Basic SQL/JSON query operators NO +T821 Basic SQL/JSON query operators YES T822 SQL/JSON: IS JSON WITH UNIQUE KEYS predicate YES -T823 SQL/JSON: PASSING clause NO -T824 JSON_TABLE: specific PLAN clause NO -T825 SQL/JSON: ON EMPTY and ON ERROR clauses NO -T826 General value expression in ON ERROR or ON EMPTY clauses NO -T827 JSON_TABLE: sibling NESTED COLUMNS clauses NO -T828 JSON_QUERY NO -T829 JSON_QUERY: array wrapper options NO +T823 SQL/JSON: PASSING clause YES +T824 JSON_TABLE: specific PLAN clause YES +T825 SQL/JSON: ON EMPTY and ON ERROR clauses YES +T826 General value expression in ON ERROR or ON EMPTY clauses YES +T827 JSON_TABLE: sibling NESTED COLUMNS clauses YES +T828 JSON_QUERY YES +T829 JSON_QUERY: array wrapper options YES T830 Enforcing unique keys in SQL/JSON constructor functions YES T831 SQL/JSON path language: strict mode YES T832 SQL/JSON path language: item method YES @@ -565,7 +565,7 @@ T834 SQL/JSON path language: wildcard member accessor YES T835 SQL/JSON path language: filter expressions YES T836 SQL/JSON path language: starts with predicate YES T837 SQL/JSON path language: regex_like predicate YES -T838 JSON_TABLE: PLAN DEFAULT clause NO +T838 JSON_TABLE: PLAN DEFAULT clause YES T839 Formatted cast of datetimes to/from character strings NO T840 Hex integer literals in SQL/JSON path language YES T851 SQL/JSON: optional keywords for default syntax YES -- 2.35.3
v8-0001-Unify-JSON-categorize-type-API-and-export-for-ext.patch (application/octet-stream, 27.6K) download From 6517ffaaa61e1792308e500f4bd6710cd16a22b7 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Tue, 18 Jul 2023 17:58:05 +0900 Subject: [PATCH v8 1/5] Unify JSON categorize type API and export for external use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This essentially removes the JsonbTypeCategory enum and jsonb_categorize_type() and integrates any jsonb-specific logic that was in jsonb_categorize_type() into json_categorize_type(), now moved to jsonfuncs.c. The remaining JsonTypeCategory enum and json_categorize_type() cover the needs of the callers in both json.c and jsonb.c. json_categorize_type() has grown a new parameter named is_jsonb for callers to engage the jsonb-specific behavior of json_categorize_type(). One notable change in the now exported API of json_categorize_type() is that it now always returns *outfuncoid even though a caller may have no need currently to see one. Co-authored-by: Álvaro Herrera <alvherre@alvh.no-ip.org> Reviewed-by: Álvaro Herrera <alvherre@alvh.no-ip.org> Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- src/backend/utils/adt/json.c | 137 ++--------------- src/backend/utils/adt/jsonb.c | 245 +++++++----------------------- src/backend/utils/adt/jsonfuncs.c | 111 ++++++++++++++ src/include/utils/jsonfuncs.h | 20 +++ src/tools/pgindent/typedefs.list | 1 - 5 files changed, 199 insertions(+), 315 deletions(-) diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index 49080e5fbf..f6bef9c148 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -19,7 +19,6 @@ #include "funcapi.h" #include "libpq/pqformat.h" #include "miscadmin.h" -#include "parser/parse_coerce.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/date.h" @@ -29,21 +28,6 @@ #include "utils/lsyscache.h" #include "utils/typcache.h" -typedef enum /* type categories for datum_to_json */ -{ - JSONTYPE_NULL, /* null, so we didn't bother to identify */ - JSONTYPE_BOOL, /* boolean (built-in types only) */ - JSONTYPE_NUMERIC, /* numeric (ditto) */ - JSONTYPE_DATE, /* we use special formatting for datetimes */ - JSONTYPE_TIMESTAMP, - JSONTYPE_TIMESTAMPTZ, - JSONTYPE_JSON, /* JSON itself (and JSONB) */ - JSONTYPE_ARRAY, /* array */ - JSONTYPE_COMPOSITE, /* composite */ - JSONTYPE_CAST, /* something with an explicit cast to JSON */ - JSONTYPE_OTHER /* all else */ -} JsonTypeCategory; - /* * Support for fast key uniqueness checking. @@ -107,9 +91,6 @@ static void array_dim_to_json(StringInfo result, int dim, int ndims, int *dims, bool use_line_feeds); static void array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds); -static void json_categorize_type(Oid typoid, - JsonTypeCategory *tcategory, - Oid *outfuncoid); static void datum_to_json(Datum val, bool is_null, StringInfo result, JsonTypeCategory tcategory, Oid outfuncoid, bool key_scalar); @@ -182,106 +163,6 @@ json_recv(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(cstring_to_text_with_len(str, nbytes)); } -/* - * Determine how we want to print values of a given type in datum_to_json. - * - * Given the datatype OID, return its JsonTypeCategory, as well as the type's - * output function OID. If the returned category is JSONTYPE_CAST, we - * return the OID of the type->JSON cast function instead. - */ -static void -json_categorize_type(Oid typoid, - JsonTypeCategory *tcategory, - Oid *outfuncoid) -{ - bool typisvarlena; - - /* Look through any domain */ - typoid = getBaseType(typoid); - - *outfuncoid = InvalidOid; - - /* - * We need to get the output function for everything except date and - * timestamp types, array and composite types, booleans, and non-builtin - * types where there's a cast to json. - */ - - switch (typoid) - { - case BOOLOID: - *tcategory = JSONTYPE_BOOL; - break; - - case INT2OID: - case INT4OID: - case INT8OID: - case FLOAT4OID: - case FLOAT8OID: - case NUMERICOID: - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - *tcategory = JSONTYPE_NUMERIC; - break; - - case DATEOID: - *tcategory = JSONTYPE_DATE; - break; - - case TIMESTAMPOID: - *tcategory = JSONTYPE_TIMESTAMP; - break; - - case TIMESTAMPTZOID: - *tcategory = JSONTYPE_TIMESTAMPTZ; - break; - - case JSONOID: - case JSONBOID: - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - *tcategory = JSONTYPE_JSON; - break; - - default: - /* Check for arrays and composites */ - if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID - || typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID) - *tcategory = JSONTYPE_ARRAY; - else if (type_is_rowtype(typoid)) /* includes RECORDOID */ - *tcategory = JSONTYPE_COMPOSITE; - else - { - /* It's probably the general case ... */ - *tcategory = JSONTYPE_OTHER; - /* but let's look for a cast to json, if it's not built-in */ - if (typoid >= FirstNormalObjectId) - { - Oid castfunc; - CoercionPathType ctype; - - ctype = find_coercion_pathway(JSONOID, typoid, - COERCION_EXPLICIT, - &castfunc); - if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc)) - { - *tcategory = JSONTYPE_CAST; - *outfuncoid = castfunc; - } - else - { - /* non builtin type with no cast */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - } - else - { - /* any other builtin type */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - } - break; - } -} - /* * Turn a Datum into JSON text, appending the string to "result". * @@ -591,7 +472,7 @@ array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds) get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign); - json_categorize_type(element_type, + json_categorize_type(element_type, false, &tcategory, &outfuncoid); deconstruct_array(v, element_type, typlen, typbyval, @@ -665,7 +546,8 @@ composite_to_json(Datum composite, StringInfo result, bool use_line_feeds) outfuncoid = InvalidOid; } else - json_categorize_type(att->atttypid, &tcategory, &outfuncoid); + json_categorize_type(att->atttypid, false, &tcategory, + &outfuncoid); datum_to_json(val, isnull, result, tcategory, outfuncoid, false); } @@ -699,7 +581,7 @@ add_json(Datum val, bool is_null, StringInfo result, outfuncoid = InvalidOid; } else - json_categorize_type(val_type, + json_categorize_type(val_type, false, &tcategory, &outfuncoid); datum_to_json(val, is_null, result, tcategory, outfuncoid, key_scalar); @@ -784,12 +666,13 @@ to_json_is_immutable(Oid typoid) JsonTypeCategory tcategory; Oid outfuncoid; - json_categorize_type(typoid, &tcategory, &outfuncoid); + json_categorize_type(typoid, false, &tcategory, &outfuncoid); switch (tcategory) { case JSONTYPE_BOOL: case JSONTYPE_JSON: + case JSONTYPE_JSONB: case JSONTYPE_NULL: return true; @@ -830,7 +713,7 @@ to_json(PG_FUNCTION_ARGS) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - json_categorize_type(val_type, + json_categorize_type(val_type, false, &tcategory, &outfuncoid); result = makeStringInfo(); @@ -880,7 +763,7 @@ json_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null) MemoryContextSwitchTo(oldcontext); appendStringInfoChar(state->str, '['); - json_categorize_type(arg_type, &state->val_category, + json_categorize_type(arg_type, false, &state->val_category, &state->val_output_func); } else @@ -1112,7 +995,7 @@ json_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine data type for argument %d", 1))); - json_categorize_type(arg_type, &state->key_category, + json_categorize_type(arg_type, false, &state->key_category, &state->key_output_func); arg_type = get_fn_expr_argtype(fcinfo->flinfo, 2); @@ -1122,7 +1005,7 @@ json_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine data type for argument %d", 2))); - json_categorize_type(arg_type, &state->val_category, + json_categorize_type(arg_type, false, &state->val_category, &state->val_output_func); appendStringInfoString(state->str, "{ "); diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index cf43c3f2de..fc64f56868 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -19,7 +19,6 @@ #include "funcapi.h" #include "libpq/pqformat.h" #include "miscadmin.h" -#include "parser/parse_coerce.h" #include "utils/builtins.h" #include "utils/date.h" #include "utils/datetime.h" @@ -37,29 +36,12 @@ typedef struct JsonbInState Node *escontext; } JsonbInState; -/* unlike with json categories, we need to treat json and jsonb differently */ -typedef enum /* type categories for datum_to_jsonb */ -{ - JSONBTYPE_NULL, /* null, so we didn't bother to identify */ - JSONBTYPE_BOOL, /* boolean (built-in types only) */ - JSONBTYPE_NUMERIC, /* numeric (ditto) */ - JSONBTYPE_DATE, /* we use special formatting for datetimes */ - JSONBTYPE_TIMESTAMP, /* we use special formatting for timestamp */ - JSONBTYPE_TIMESTAMPTZ, /* ... and timestamptz */ - JSONBTYPE_JSON, /* JSON */ - JSONBTYPE_JSONB, /* JSONB */ - JSONBTYPE_ARRAY, /* array */ - JSONBTYPE_COMPOSITE, /* composite */ - JSONBTYPE_JSONCAST, /* something with an explicit cast to JSON */ - JSONBTYPE_OTHER /* all else */ -} JsonbTypeCategory; - typedef struct JsonbAggState { JsonbInState *res; - JsonbTypeCategory key_category; + JsonTypeCategory key_category; Oid key_output_func; - JsonbTypeCategory val_category; + JsonTypeCategory val_category; Oid val_output_func; } JsonbAggState; @@ -72,19 +54,13 @@ static JsonParseErrorType jsonb_in_array_end(void *pstate); static JsonParseErrorType jsonb_in_object_field_start(void *pstate, char *fname, bool isnull); static void jsonb_put_escaped_value(StringInfo out, JsonbValue *scalarVal); static JsonParseErrorType jsonb_in_scalar(void *pstate, char *token, JsonTokenType tokentype); -static void jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid); static void composite_to_jsonb(Datum composite, JsonbInState *result); static void array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *dims, Datum *vals, bool *nulls, int *valcount, - JsonbTypeCategory tcategory, Oid outfuncoid); + JsonTypeCategory tcategory, Oid outfuncoid); static void array_to_jsonb_internal(Datum array, JsonbInState *result); -static void jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid); static void datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, - JsonbTypeCategory tcategory, Oid outfuncoid, + JsonTypeCategory tcategory, Oid outfuncoid, bool key_scalar); static void add_jsonb(Datum val, bool is_null, JsonbInState *result, Oid val_type, bool key_scalar); @@ -633,112 +609,6 @@ add_indent(StringInfo out, bool indent, int level) } -/* - * Determine how we want to render values of a given type in datum_to_jsonb. - * - * Given the datatype OID, return its JsonbTypeCategory, as well as the type's - * output function OID. If the returned category is JSONBTYPE_JSONCAST, - * we return the OID of the relevant cast function instead. - */ -static void -jsonb_categorize_type(Oid typoid, - JsonbTypeCategory *tcategory, - Oid *outfuncoid) -{ - bool typisvarlena; - - /* Look through any domain */ - typoid = getBaseType(typoid); - - *outfuncoid = InvalidOid; - - /* - * We need to get the output function for everything except date and - * timestamp types, booleans, array and composite types, json and jsonb, - * and non-builtin types where there's a cast to json. In this last case - * we return the oid of the cast function instead. - */ - - switch (typoid) - { - case BOOLOID: - *tcategory = JSONBTYPE_BOOL; - break; - - case INT2OID: - case INT4OID: - case INT8OID: - case FLOAT4OID: - case FLOAT8OID: - case NUMERICOID: - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - *tcategory = JSONBTYPE_NUMERIC; - break; - - case DATEOID: - *tcategory = JSONBTYPE_DATE; - break; - - case TIMESTAMPOID: - *tcategory = JSONBTYPE_TIMESTAMP; - break; - - case TIMESTAMPTZOID: - *tcategory = JSONBTYPE_TIMESTAMPTZ; - break; - - case JSONBOID: - *tcategory = JSONBTYPE_JSONB; - break; - - case JSONOID: - *tcategory = JSONBTYPE_JSON; - break; - - default: - /* Check for arrays and composites */ - if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID - || typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID) - *tcategory = JSONBTYPE_ARRAY; - else if (type_is_rowtype(typoid)) /* includes RECORDOID */ - *tcategory = JSONBTYPE_COMPOSITE; - else - { - /* It's probably the general case ... */ - *tcategory = JSONBTYPE_OTHER; - - /* - * but first let's look for a cast to json (note: not to - * jsonb) if it's not built-in. - */ - if (typoid >= FirstNormalObjectId) - { - Oid castfunc; - CoercionPathType ctype; - - ctype = find_coercion_pathway(JSONOID, typoid, - COERCION_EXPLICIT, &castfunc); - if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc)) - { - *tcategory = JSONBTYPE_JSONCAST; - *outfuncoid = castfunc; - } - else - { - /* not a cast type, so just get the usual output func */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - } - else - { - /* any other builtin type */ - getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); - } - break; - } - } -} - /* * Turn a Datum into jsonb, adding it to the result JsonbInState. * @@ -753,7 +623,7 @@ jsonb_categorize_type(Oid typoid, */ static void datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, - JsonbTypeCategory tcategory, Oid outfuncoid, + JsonTypeCategory tcategory, Oid outfuncoid, bool key_scalar) { char *outputstr; @@ -770,11 +640,11 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, jb.type = jbvNull; } else if (key_scalar && - (tcategory == JSONBTYPE_ARRAY || - tcategory == JSONBTYPE_COMPOSITE || - tcategory == JSONBTYPE_JSON || - tcategory == JSONBTYPE_JSONB || - tcategory == JSONBTYPE_JSONCAST)) + (tcategory == JSONTYPE_ARRAY || + tcategory == JSONTYPE_COMPOSITE || + tcategory == JSONTYPE_JSON || + tcategory == JSONTYPE_JSONB || + tcategory == JSONTYPE_JSON)) { ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -782,18 +652,18 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, } else { - if (tcategory == JSONBTYPE_JSONCAST) + if (tcategory == JSONTYPE_CAST) val = OidFunctionCall1(outfuncoid, val); switch (tcategory) { - case JSONBTYPE_ARRAY: + case JSONTYPE_ARRAY: array_to_jsonb_internal(val, result); break; - case JSONBTYPE_COMPOSITE: + case JSONTYPE_COMPOSITE: composite_to_jsonb(val, result); break; - case JSONBTYPE_BOOL: + case JSONTYPE_BOOL: if (key_scalar) { outputstr = DatumGetBool(val) ? "true" : "false"; @@ -807,7 +677,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, jb.val.boolean = DatumGetBool(val); } break; - case JSONBTYPE_NUMERIC: + case JSONTYPE_NUMERIC: outputstr = OidOutputFunctionCall(outfuncoid, val); if (key_scalar) { @@ -845,26 +715,26 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, } } break; - case JSONBTYPE_DATE: + case JSONTYPE_DATE: jb.type = jbvString; jb.val.string.val = JsonEncodeDateTime(NULL, val, DATEOID, NULL); jb.val.string.len = strlen(jb.val.string.val); break; - case JSONBTYPE_TIMESTAMP: + case JSONTYPE_TIMESTAMP: jb.type = jbvString; jb.val.string.val = JsonEncodeDateTime(NULL, val, TIMESTAMPOID, NULL); jb.val.string.len = strlen(jb.val.string.val); break; - case JSONBTYPE_TIMESTAMPTZ: + case JSONTYPE_TIMESTAMPTZ: jb.type = jbvString; jb.val.string.val = JsonEncodeDateTime(NULL, val, TIMESTAMPTZOID, NULL); jb.val.string.len = strlen(jb.val.string.val); break; - case JSONBTYPE_JSONCAST: - case JSONBTYPE_JSON: + case JSONTYPE_CAST: + case JSONTYPE_JSON: { /* parse the json right into the existing result object */ JsonLexContext *lex; @@ -887,7 +757,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, pg_parse_json_or_ereport(lex, &sem); } break; - case JSONBTYPE_JSONB: + case JSONTYPE_JSONB: { Jsonb *jsonb = DatumGetJsonbP(val); JsonbIterator *it; @@ -931,7 +801,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, /* Now insert jb into result, unless we did it recursively */ if (!is_null && !scalar_jsonb && - tcategory >= JSONBTYPE_JSON && tcategory <= JSONBTYPE_JSONCAST) + tcategory >= JSONTYPE_JSON && tcategory <= JSONTYPE_CAST) { /* work has been done recursively */ return; @@ -976,7 +846,7 @@ datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, */ static void array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *dims, Datum *vals, - bool *nulls, int *valcount, JsonbTypeCategory tcategory, + bool *nulls, int *valcount, JsonTypeCategory tcategory, Oid outfuncoid) { int i; @@ -1020,7 +890,7 @@ array_to_jsonb_internal(Datum array, JsonbInState *result) int16 typlen; bool typbyval; char typalign; - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; ndim = ARR_NDIM(v); @@ -1037,8 +907,8 @@ array_to_jsonb_internal(Datum array, JsonbInState *result) get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign); - jsonb_categorize_type(element_type, - &tcategory, &outfuncoid); + json_categorize_type(element_type, true, + &tcategory, &outfuncoid); deconstruct_array(v, element_type, typlen, typbyval, typalign, &elements, &nulls, @@ -1084,7 +954,7 @@ composite_to_jsonb(Datum composite, JsonbInState *result) Datum val; bool isnull; char *attname; - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; JsonbValue v; Form_pg_attribute att = TupleDescAttr(tupdesc, i); @@ -1105,11 +975,12 @@ composite_to_jsonb(Datum composite, JsonbInState *result) if (isnull) { - tcategory = JSONBTYPE_NULL; + tcategory = JSONTYPE_NULL; outfuncoid = InvalidOid; } else - jsonb_categorize_type(att->atttypid, &tcategory, &outfuncoid); + json_categorize_type(att->atttypid, true, &tcategory, + &outfuncoid); datum_to_jsonb(val, isnull, result, tcategory, outfuncoid, false); } @@ -1122,7 +993,7 @@ composite_to_jsonb(Datum composite, JsonbInState *result) * Append JSON text for "val" to "result". * * This is just a thin wrapper around datum_to_jsonb. If the same type will be - * printed many times, avoid using this; better to do the jsonb_categorize_type + * printed many times, avoid using this; better to do the json_categorize_type * lookups only once. */ @@ -1130,7 +1001,7 @@ static void add_jsonb(Datum val, bool is_null, JsonbInState *result, Oid val_type, bool key_scalar) { - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; if (val_type == InvalidOid) @@ -1140,12 +1011,12 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, if (is_null) { - tcategory = JSONBTYPE_NULL; + tcategory = JSONTYPE_NULL; outfuncoid = InvalidOid; } else - jsonb_categorize_type(val_type, - &tcategory, &outfuncoid); + json_categorize_type(val_type, true, + &tcategory, &outfuncoid); datum_to_jsonb(val, is_null, result, tcategory, outfuncoid, key_scalar); } @@ -1160,33 +1031,33 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, bool to_jsonb_is_immutable(Oid typoid) { - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; - jsonb_categorize_type(typoid, &tcategory, &outfuncoid); + json_categorize_type(typoid, true, &tcategory, &outfuncoid); switch (tcategory) { - case JSONBTYPE_NULL: - case JSONBTYPE_BOOL: - case JSONBTYPE_JSON: - case JSONBTYPE_JSONB: + case JSONTYPE_NULL: + case JSONTYPE_BOOL: + case JSONTYPE_JSON: + case JSONTYPE_JSONB: return true; - case JSONBTYPE_DATE: - case JSONBTYPE_TIMESTAMP: - case JSONBTYPE_TIMESTAMPTZ: + case JSONTYPE_DATE: + case JSONTYPE_TIMESTAMP: + case JSONTYPE_TIMESTAMPTZ: return false; - case JSONBTYPE_ARRAY: + case JSONTYPE_ARRAY: return false; /* TODO recurse into elements */ - case JSONBTYPE_COMPOSITE: + case JSONTYPE_COMPOSITE: return false; /* TODO recurse into fields */ - case JSONBTYPE_NUMERIC: - case JSONBTYPE_JSONCAST: - case JSONBTYPE_OTHER: + case JSONTYPE_NUMERIC: + case JSONTYPE_CAST: + case JSONTYPE_OTHER: return func_volatile(outfuncoid) == PROVOLATILE_IMMUTABLE; } @@ -1202,7 +1073,7 @@ to_jsonb(PG_FUNCTION_ARGS) Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); JsonbInState result; - JsonbTypeCategory tcategory; + JsonTypeCategory tcategory; Oid outfuncoid; if (val_type == InvalidOid) @@ -1210,8 +1081,8 @@ to_jsonb(PG_FUNCTION_ARGS) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - jsonb_categorize_type(val_type, - &tcategory, &outfuncoid); + json_categorize_type(val_type, true, + &tcategory, &outfuncoid); memset(&result, 0, sizeof(JsonbInState)); @@ -1636,8 +1507,8 @@ jsonb_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null) WJB_BEGIN_ARRAY, NULL); MemoryContextSwitchTo(oldcontext); - jsonb_categorize_type(arg_type, &state->val_category, - &state->val_output_func); + json_categorize_type(arg_type, true, &state->val_category, + &state->val_output_func); } else { @@ -1816,8 +1687,8 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - jsonb_categorize_type(arg_type, &state->key_category, - &state->key_output_func); + json_categorize_type(arg_type, true, &state->key_category, + &state->key_output_func); arg_type = get_fn_expr_argtype(fcinfo->flinfo, 2); @@ -1826,8 +1697,8 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - jsonb_categorize_type(arg_type, &state->val_category, - &state->val_output_func); + json_categorize_type(arg_type, true, &state->val_category, + &state->val_output_func); } else { diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index 70cb922e6b..a4bfa5e404 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -26,6 +26,7 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" #include "nodes/miscnodes.h" +#include "parser/parse_coerce.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -5685,3 +5686,113 @@ json_get_first_token(text *json, bool throw_error) return JSON_TOKEN_INVALID; /* invalid json */ } + +/* + * Determine how we want to print values of a given type in datum_to_json(b). + * + * Given the datatype OID, return its JsonTypeCategory, as well as the type's + * output function OID. If the returned category is JSONTYPE_CAST, we return + * the OID of the type->JSON cast function instead. + */ +void +json_categorize_type(Oid typoid, bool is_jsonb, + JsonTypeCategory *tcategory, Oid *outfuncoid) +{ + bool typisvarlena; + + /* Look through any domain */ + typoid = getBaseType(typoid); + + *outfuncoid = InvalidOid; + + switch (typoid) + { + case BOOLOID: + *outfuncoid = F_BOOLOUT; + *tcategory = JSONTYPE_BOOL; + break; + + case INT2OID: + case INT4OID: + case INT8OID: + case FLOAT4OID: + case FLOAT8OID: + case NUMERICOID: + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + *tcategory = JSONTYPE_NUMERIC; + break; + + case DATEOID: + *outfuncoid = F_DATE_OUT; + *tcategory = JSONTYPE_DATE; + break; + + case TIMESTAMPOID: + *outfuncoid = F_TIMESTAMP_OUT; + *tcategory = JSONTYPE_TIMESTAMP; + break; + + case TIMESTAMPTZOID: + *outfuncoid = F_TIMESTAMPTZ_OUT; + *tcategory = JSONTYPE_TIMESTAMPTZ; + break; + + case JSONOID: + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + *tcategory = JSONTYPE_JSON; + break; + + case JSONBOID: + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + *tcategory = is_jsonb ? JSONTYPE_JSONB : JSONTYPE_JSON; + break; + + default: + /* Check for arrays and composites */ + if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID + || typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID) + { + *outfuncoid = F_ARRAY_OUT; + *tcategory = JSONTYPE_ARRAY; + } + else if (type_is_rowtype(typoid)) /* includes RECORDOID */ + { + *outfuncoid = F_RECORD_OUT; + *tcategory = JSONTYPE_COMPOSITE; + } + else + { + /* + * It's probably the general case. But let's look for a cast + * to json (note: not to jsonb even if is_jsonb is true), if + * it's not built-in. + */ + *tcategory = JSONTYPE_OTHER; + if (typoid >= FirstNormalObjectId) + { + Oid castfunc; + CoercionPathType ctype; + + ctype = find_coercion_pathway(JSONOID, typoid, + COERCION_EXPLICIT, + &castfunc); + if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc)) + { + *outfuncoid = castfunc; + *tcategory = JSONTYPE_CAST; + } + else + { + /* non builtin type with no cast */ + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + } + } + else + { + /* any other builtin type */ + getTypeOutputInfo(typoid, outfuncoid, &typisvarlena); + } + } + break; + } +} diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index a85203d4a4..27a25ba283 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -63,4 +63,24 @@ extern Jsonb *transform_jsonb_string_values(Jsonb *jsonb, void *action_state, extern text *transform_json_string_values(text *json, void *action_state, JsonTransformStringValuesAction transform_action); +/* Type categories returned by json_categorize_type */ +typedef enum +{ + JSONTYPE_NULL, /* null, so we didn't bother to identify */ + JSONTYPE_BOOL, /* boolean (built-in types only) */ + JSONTYPE_NUMERIC, /* numeric (ditto) */ + JSONTYPE_DATE, /* we use special formatting for datetimes */ + JSONTYPE_TIMESTAMP, + JSONTYPE_TIMESTAMPTZ, + JSONTYPE_JSON, /* JSON (and JSONB, if not is_jsonb) */ + JSONTYPE_JSONB, /* JSONB (if is_jsonb) */ + JSONTYPE_ARRAY, /* array */ + JSONTYPE_COMPOSITE, /* composite */ + JSONTYPE_CAST, /* something with an explicit cast to JSON */ + JSONTYPE_OTHER, /* all else */ +} JsonTypeCategory; + +void json_categorize_type(Oid typoid, bool is_jsonb, + JsonTypeCategory *tcategory, Oid *outfuncoid); + #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e941fb6c82..b10590a252 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1318,7 +1318,6 @@ JsonbIteratorToken JsonbPair JsonbParseState JsonbSubWorkspace -JsonbTypeCategory JsonbValue JumbleState JunkFilter -- 2.35.3
v8-0002-SQL-JSON-functions.patch (application/octet-stream, 50.5K) download From b93448b7b2c5dacac0bce60070ec76fdc763d16f Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Tue, 18 Jul 2023 17:58:28 +0900 Subject: [PATCH v8 2/5] SQL JSON functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This Patch introduces three SQL standard JSON functions: JSON() JSON_SCALAR() JSON_SERIALIZE() JSON() produces json values from text, bytea, json or jsonb values, and has facilitites for handling duplicate keys. JSON_SCALAR() produces a json value from any scalar sql value, including json and jsonb. JSON_SERIALIZE() produces text or bytea from input which containis or represents json or jsonb; For the most part these functions don't add any significant new capabilities, but they will be of use to users wanting standard compliant JSON handling. Author: Nikita Glukhov <n.gluhov@postgrespro.ru> Author: Teodor Sigaev <teodor@sigaev.ru> Author: Oleg Bartunov <obartunov@gmail.com> Author: Alexander Korotkov <aekorotkov@gmail.com> Author: Andrew Dunstan <andrew@dunslane.net> Author: Amit Langote <amitlangote09@gmail.com> Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- doc/src/sgml/func.sgml | 65 +++++ src/backend/executor/execExpr.c | 30 +++ src/backend/executor/execExprInterp.c | 43 +++- src/backend/nodes/nodeFuncs.c | 30 +++ src/backend/parser/gram.y | 68 +++++- src/backend/parser/parse_expr.c | 208 ++++++++++++++-- src/backend/parser/parse_target.c | 9 + src/backend/utils/adt/format_type.c | 4 + src/backend/utils/adt/json.c | 21 +- src/backend/utils/adt/jsonb.c | 48 +++- src/backend/utils/adt/ruleutils.c | 14 +- src/include/nodes/parsenodes.h | 48 ++++ src/include/nodes/primnodes.h | 5 +- src/include/parser/kwlist.h | 4 +- src/include/utils/jsonb.h | 2 +- src/include/utils/jsonfuncs.h | 4 + src/test/regress/expected/sqljson.out | 332 ++++++++++++++++++++++++++ src/test/regress/sql/sqljson.sql | 85 +++++++ src/tools/pgindent/typedefs.list | 1 + 19 files changed, 972 insertions(+), 49 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 0b62e0c828..ac81f6b827 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16001,6 +16001,71 @@ table2-mapping <returnvalue>{"a": "1", "b": "2"}</returnvalue> </para></entry> </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json constructor</primary></indexterm> + <function>json</function> ( + <replaceable>expression</replaceable> + <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional></optional> + <optional> { <literal>WITH</literal> | <literal>WITHOUT</literal> } <literal>UNIQUE</literal> <optional> <literal>KEYS</literal> </optional></optional> + </para> + <para> + The <replaceable>expression</replaceable> can be any text type or a + <type>bytea</type> in UTF8 encoding. If the + <replaceable>expression</replaceable> is NULL, an + <acronym>SQL</acronym> null value is returned. + If <literal>WITH UNIQUE</literal> is specified, the + <replaceable>expression</replaceable> must not contain any duplicate + object keys. + </para> + <para> + <literal>json('{"a":123, "b":[true,"foo"], "a":"bar"}')</literal> + <returnvalue>{"a":123, "b":[true,"foo"], "a":"bar"}</returnvalue> + </para> + </entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json_scalar</primary></indexterm> + <function>json_scalar</function> (<replaceable>expression</replaceable>) + </para> + <para> + Returns a JSON scalar value representing + <replaceable>expression</replaceable>. + If the input is NULL, an SQL NULL is returned. If the input is a number + or a boolean value, a corresponding JSON number or boolean value is + returned. For any other value a JSON string is returned. + </para> + <para> + <literal>json_scalar(123.45)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_scalar(CURRENT_TIMESTAMP)</literal> + <returnvalue>"2022-05-10T10:51:04.62128-04:00"</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <function>json_serialize</function> ( + <replaceable>expression</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional>) + </para> + <para> + Transforms an SQL/JSON value into a character or binary string. The + <replaceable>expression</replaceable> can be of any JSON type, any + character string type, or <type>bytea</type> in UTF8 encoding. + The returned type can be any character string type or + <type>bytea</type>. The default is <type>text</type>. + </para> + <para> + <literal>json_serialize('{ "a" : 1 } ' RETURNING bytea)</literal> + <returnvalue>\x7b20226122203a2031207d20</returnvalue> + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index bf3a08c5f0..6ca4098bef 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -48,6 +48,7 @@ #include "utils/array.h" #include "utils/builtins.h" #include "utils/datum.h" +#include "utils/jsonfuncs.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -2311,6 +2312,12 @@ ExecInitExprRec(Expr *node, ExprState *state, { ExecInitExprRec(ctor->func, state, resv, resnull); } + else if ((ctor->type == JSCTOR_JSON_PARSE && !ctor->unique) || + ctor->type == JSCTOR_JSON_SERIALIZE) + { + /* Use the value of the first argument as a result */ + ExecInitExprRec(linitial(args), state, resv, resnull); + } else { JsonConstructorExprState *jcstate; @@ -2349,6 +2356,29 @@ ExecInitExprRec(Expr *node, ExprState *state, argno++; } + /* prepare type cache for datum_to_json[b]() */ + if (ctor->type == JSCTOR_JSON_SCALAR) + { + bool is_jsonb = + ctor->returning->format->format_type == JS_FORMAT_JSONB; + + jcstate->arg_type_cache = + palloc(sizeof(*jcstate->arg_type_cache) * nargs); + + for (int i = 0; i < nargs; i++) + { + JsonTypeCategory category; + Oid outfuncid; + Oid typid = jcstate->arg_types[i]; + + json_categorize_type(typid, is_jsonb, + &category, &outfuncid); + + jcstate->arg_type_cache[i].outfuncid = outfuncid; + jcstate->arg_type_cache[i].category = (int) category; + } + } + ExprEvalPushStep(state, &scratch); } diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 851946a927..76e59691e5 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -3992,7 +3992,7 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_values, jcstate->arg_nulls, jcstate->arg_types, - jcstate->constructor->absent_on_null); + ctor->absent_on_null); else if (ctor->type == JSCTOR_JSON_OBJECT) res = (is_jsonb ? jsonb_build_object_worker : @@ -4002,6 +4002,47 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_types, jcstate->constructor->absent_on_null, jcstate->constructor->unique); + else if (ctor->type == JSCTOR_JSON_SCALAR) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + Oid outfuncid = jcstate->arg_type_cache[0].outfuncid; + JsonTypeCategory category = (JsonTypeCategory) + jcstate->arg_type_cache[0].category; + + if (is_jsonb) + res = to_jsonb_worker(value, category, outfuncid); + else + res = to_json_worker(value, category, outfuncid); + } + } + else if (ctor->type == JSCTOR_JSON_PARSE) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + text *js = DatumGetTextP(value); + + if (is_jsonb) + res = jsonb_from_text(js, true); + else + { + (void) json_validate(js, true, true); + res = value; + } + } + } else elog(ERROR, "invalid JsonConstructorExpr type %d", ctor->type); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index c41e6bb984..dda964bd19 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -3901,6 +3901,36 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonParseExpr: + { + JsonParseExpr *jpe = (JsonParseExpr *) node; + + if (WALK(jpe->expr)) + return true; + if (WALK(jpe->output)) + return true; + } + break; + case T_JsonScalarExpr: + { + JsonScalarExpr *jse = (JsonScalarExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; + case T_JsonSerializeExpr: + { + JsonSerializeExpr *jse = (JsonSerializeExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; case T_JsonConstructorExpr: { JsonConstructorExpr *ctor = (JsonConstructorExpr *) node; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index edb6c00ece..26cbebb707 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -566,7 +566,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <list> copy_options %type <typnam> Typename SimpleTypename ConstTypename - GenericType Numeric opt_float + GenericType Numeric opt_float JsonType Character ConstCharacter CharacterWithLength CharacterWithoutLength ConstDatetime ConstInterval @@ -647,7 +647,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> json_format_clause_opt json_value_expr - json_output_clause_opt + json_returning_clause_opt json_name_and_value json_aggregate_func %type <list> json_name_and_value_list @@ -659,7 +659,6 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt - /* * Non-keyword token types. These are hard-wired into the "flex" lexer. * They must be listed first so that their numeric codes do not depend on @@ -723,6 +722,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG + JSON_SCALAR JSON_SERIALIZE KEY KEYS @@ -13981,6 +13981,7 @@ SimpleTypename: $$->typmods = list_make2(makeIntConst(INTERVAL_FULL_RANGE, -1), makeIntConst($3, @3)); } + | JsonType { $$ = $1; } ; /* We have a separate ConstTypename to allow defaulting fixed-length @@ -13999,6 +14000,7 @@ ConstTypename: | ConstBit { $$ = $1; } | ConstCharacter { $$ = $1; } | ConstDatetime { $$ = $1; } + | JsonType { $$ = $1; } ; /* @@ -14367,6 +14369,13 @@ interval_second: } ; +JsonType: + JSON + { + $$ = SystemTypeName("json"); + $$->location = @1; + } + ; /***************************************************************************** * @@ -15561,7 +15570,7 @@ func_expr_common_subexpr: | JSON_OBJECT '(' json_name_and_value_list json_object_constructor_null_clause_opt json_key_uniqueness_constraint_opt - json_output_clause_opt ')' + json_returning_clause_opt ')' { JsonObjectConstructor *n = makeNode(JsonObjectConstructor); @@ -15572,7 +15581,7 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } - | JSON_OBJECT '(' json_output_clause_opt ')' + | JSON_OBJECT '(' json_returning_clause_opt ')' { JsonObjectConstructor *n = makeNode(JsonObjectConstructor); @@ -15586,7 +15595,7 @@ func_expr_common_subexpr: | JSON_ARRAY '(' json_value_expr_list json_array_constructor_null_clause_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayConstructor *n = makeNode(JsonArrayConstructor); @@ -15601,7 +15610,7 @@ func_expr_common_subexpr: select_no_parens json_format_clause_opt /* json_array_constructor_null_clause_opt */ - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayQueryConstructor *n = makeNode(JsonArrayQueryConstructor); @@ -15614,7 +15623,7 @@ func_expr_common_subexpr: $$ = (Node *) n; } | JSON_ARRAY '(' - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayConstructor *n = makeNode(JsonArrayConstructor); @@ -15625,7 +15634,36 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } - ; + | JSON '(' json_value_expr json_key_uniqueness_constraint_opt ')' + { + JsonParseExpr *n = makeNode(JsonParseExpr); + + n->expr = (JsonValueExpr *) $3; + n->unique_keys = $4; + n->output = NULL; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SCALAR '(' a_expr ')' + { + JsonScalarExpr *n = makeNode(JsonScalarExpr); + + n->expr = (Expr *) $3; + n->output = NULL; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SERIALIZE '(' json_value_expr json_returning_clause_opt ')' + { + JsonSerializeExpr *n = makeNode(JsonSerializeExpr); + + n->expr = (JsonValueExpr *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + ; + /* * SQL/XML support @@ -16373,7 +16411,7 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; -json_output_clause_opt: +json_returning_clause_opt: RETURNING Typename json_format_clause_opt { JsonOutput *n = makeNode(JsonOutput); @@ -16446,7 +16484,7 @@ json_aggregate_func: json_name_and_value json_object_constructor_null_clause_opt json_key_uniqueness_constraint_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonObjectAgg *n = makeNode(JsonObjectAgg); @@ -16464,7 +16502,7 @@ json_aggregate_func: json_value_expr json_array_aggregate_order_by_clause_opt json_array_constructor_null_clause_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayAgg *n = makeNode(JsonArrayAgg); @@ -17064,7 +17102,6 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION - | JSON | KEY | KEYS | LABEL @@ -17279,10 +17316,13 @@ col_name_keyword: | INT_P | INTEGER | INTERVAL + | JSON | JSON_ARRAY | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | LEAST | NATIONAL | NCHAR @@ -17643,6 +17683,8 @@ bare_label_keyword: | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | KEY | KEYS | LABEL diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 5a05caa874..a2644dbc77 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -86,6 +86,10 @@ static Node *transformJsonArrayQueryConstructor(ParseState *pstate, static Node *transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg); static Node *transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg); static Node *transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred); +static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); +static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); +static Node *transformJsonSerializeExpr(ParseState *pstate, + JsonSerializeExpr * expr); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -337,6 +341,18 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonIsPredicate(pstate, (JsonIsPredicate *) expr); break; + case T_JsonParseExpr: + result = transformJsonParseExpr(pstate, (JsonParseExpr *) expr); + break; + + case T_JsonScalarExpr: + result = transformJsonScalarExpr(pstate, (JsonScalarExpr *) expr); + break; + + case T_JsonSerializeExpr: + result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3208,7 +3224,8 @@ makeJsonByteaToTextConversion(Node *expr, JsonFormat *format, int location) */ static Node * transformJsonValueExpr(ParseState *pstate, const char *constructName, - JsonValueExpr *ve, JsonFormatType default_format) + JsonValueExpr *ve, JsonFormatType default_format, + Oid targettype) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3250,12 +3267,14 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, else format = default_format; - if (format != JS_FORMAT_DEFAULT) + if (format != JS_FORMAT_DEFAULT || + (OidIsValid(targettype) && exprtype != targettype)) { - Oid targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; Node *coerced; + bool cast_is_needed = OidIsValid(targettype); - if (exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) + if (!cast_is_needed && + exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), errmsg(ve->format->format_type == JS_FORMAT_DEFAULT ? @@ -3271,6 +3290,9 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, exprtype = TEXTOID; } + if (!OidIsValid(targettype)) + targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; + /* Try to coerce to the target type. */ coerced = coerce_to_target_type(pstate, expr, exprtype, targettype, -1, @@ -3281,11 +3303,20 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, if (!coerced) { /* If coercion failed, use to_json()/to_jsonb() functions. */ - Oid fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; - FuncExpr *fexpr = makeFuncExpr(fnoid, targettype, - list_make1(expr), - InvalidOid, InvalidOid, - COERCE_EXPLICIT_CALL); + FuncExpr *fexpr; + Oid fnoid; + + if (cast_is_needed) /* only CAST is allowed */ + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(exprtype), + format_type_be(targettype)), + parser_errposition(pstate, location))); + + fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; + fexpr = makeFuncExpr(fnoid, targettype, list_make1(expr), + InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL); fexpr->location = location; @@ -3582,7 +3613,8 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) Node *key = transformExprRecurse(pstate, (Node *) kv->key); Node *val = transformJsonValueExpr(pstate, "JSON_OBJECT()", kv->value, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, key); args = lappend(args, val); @@ -3767,9 +3799,8 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) Oid aggtype; key = transformExprRecurse(pstate, (Node *) agg->arg->key); - val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", - agg->arg->value, - JS_FORMAT_DEFAULT); + val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", agg->arg->value, + JS_FORMAT_DEFAULT, InvalidOid); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3827,7 +3858,7 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", agg->arg, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, InvalidOid); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3875,7 +3906,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, "JSON_ARRAY()", jsval, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, val); } @@ -3958,3 +3990,149 @@ transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred) return makeJsonIsPredicate(expr, NULL, pred->item_type, pred->unique_keys, pred->location); } + +/* + * Transform the output clause of a JSON_*() expression if there is one and + * create one if not. + */ +static JsonReturning * +transformJsonReturning(ParseState *pstate, JsonOutput *output, const char *fname) +{ + JsonReturning *returning; + + if (output) + { + returning = transformJsonOutput(pstate, output, false); + + Assert(OidIsValid(returning->typid)); + + if (returning->typid != JSONOID && returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), fname), + parser_errposition(pstate, output->typeName->location))); + } + else + { + /* Output type is JSON by default. */ + Oid targettype = JSONOID; + JsonFormatType format = JS_FORMAT_JSON; + + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(format, JS_ENC_DEFAULT, -1); + returning->typid = targettype; + returning->typmod = -1; + } + + return returning; +} + +/* + * Transform a JSON() expression. + */ +static Node * +transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) +{ + JsonOutput *output = jsexpr->output; + JsonReturning *returning; + Node *arg; + + returning = transformJsonReturning(pstate, output, "JSON()"); + + if (jsexpr->unique_keys) + { + /* + * Coerce string argument to text and then to json[b] in the executor + * node with key uniqueness check. + */ + JsonValueExpr *jve = jsexpr->expr; + Oid arg_type; + + arg = transformJsonParseArg(pstate, (Node *) jve->raw_expr, jve->format, + &arg_type); + + if (arg_type != TEXTOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use non-string types with WITH UNIQUE KEYS clause"), + parser_errposition(pstate, jsexpr->location))); + } + else + { + /* + * Coerce argument to target type using CAST for compatibility with PG + * function-like CASTs. + */ + arg = transformJsonValueExpr(pstate, "JSON()", jsexpr->expr, + JS_FORMAT_JSON, returning->typid); + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, + returning, jsexpr->unique_keys, false, + jsexpr->location); +} + +/* + * Transform a JSON_SCALAR() expression. + */ +static Node * +transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *jsexpr) +{ + Node *arg = transformExprRecurse(pstate, (Node *) jsexpr->expr); + JsonOutput *output = jsexpr->output; + JsonReturning *returning; + + returning = transformJsonReturning(pstate, output, "JSON_SCALAR()"); + + if (exprType(arg) == UNKNOWNOID) + arg = coerce_to_specific_type(pstate, arg, TEXTOID, "JSON_SCALAR"); + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SCALAR, list_make1(arg), NULL, + returning, false, false, jsexpr->location); +} + +/* + * Transform a JSON_SERIALIZE() expression. + */ +static Node * +transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) +{ + Node *arg = transformJsonValueExpr(pstate, "JSON_SERIALIZE()", + expr->expr, + JS_FORMAT_JSON, + InvalidOid); + JsonReturning *returning; + + if (expr->output) + { + returning = transformJsonOutput(pstate, expr->output, true); + + if (returning->typid != BYTEAOID) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(returning->typid, &typcategory, + &typispreferred); + if (typcategory != TYPCATEGORY_STRING) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), + "JSON_SERIALIZE()"), + errhint("Try returning a string type or bytea."))); + } + } + else + { + /* RETURNING TEXT FORMAT JSON is by default */ + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(JS_FORMAT_JSON, JS_ENC_DEFAULT, -1); + returning->typid = TEXTOID; + returning->typmod = -1; + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), + NULL, returning, false, false, expr->location); +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4cca97ff9c..520d4f2a23 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1953,6 +1953,15 @@ FigureColnameInternal(Node *node, char **name) /* make XMLSERIALIZE act like a regular function */ *name = "xmlserialize"; return 2; + case T_JsonParseExpr: + *name = "json"; + return 2; + case T_JsonScalarExpr: + *name = "json_scalar"; + return 2; + case T_JsonSerializeExpr: + *name = "json_serialize"; + return 2; case T_JsonObjectConstructor: /* make JSON_OBJECT act like a regular function */ *name = "json_object"; diff --git a/src/backend/utils/adt/format_type.c b/src/backend/utils/adt/format_type.c index 12402a0637..36c45a3978 100644 --- a/src/backend/utils/adt/format_type.c +++ b/src/backend/utils/adt/format_type.c @@ -294,6 +294,10 @@ format_type_extended(Oid type_oid, int32 typemod, bits16 flags) else buf = pstrdup("character varying"); break; + + case JSONOID: + buf = pstrdup("json"); + break; } if (buf == NULL) diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index f6bef9c148..c316f848e1 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -653,6 +653,20 @@ row_to_json_pretty(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); } +/* + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. + */ +Datum +to_json_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid) +{ + StringInfo result = makeStringInfo(); + + datum_to_json(val, false, result, tcategory, outfuncoid, false); + + return PointerGetDatum(cstring_to_text_with_len(result->data, result->len)); +} + /* * Is the given type immutable when coming out of a JSON context? * @@ -704,7 +718,6 @@ to_json(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - StringInfo result; JsonTypeCategory tcategory; Oid outfuncoid; @@ -716,11 +729,7 @@ to_json(PG_FUNCTION_ARGS) json_categorize_type(val_type, false, &tcategory, &outfuncoid); - result = makeStringInfo(); - - datum_to_json(val, false, result, tcategory, outfuncoid, false); - - PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); + PG_RETURN_DATUM(to_json_worker(val, tcategory, outfuncoid)); } /* diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index fc64f56868..06ba409e64 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -33,6 +33,7 @@ typedef struct JsonbInState { JsonbParseState *parseState; JsonbValue *res; + bool unique_keys; Node *escontext; } JsonbInState; @@ -45,7 +46,8 @@ typedef struct JsonbAggState Oid val_output_func; } JsonbAggState; -static inline Datum jsonb_from_cstring(char *json, int len, Node *escontext); +static inline Datum jsonb_from_cstring(char *json, int len, bool unique_keys, + Node *escontext); static bool checkStringLen(size_t len, Node *escontext); static JsonParseErrorType jsonb_in_object_start(void *pstate); static JsonParseErrorType jsonb_in_object_end(void *pstate); @@ -76,7 +78,7 @@ jsonb_in(PG_FUNCTION_ARGS) { char *json = PG_GETARG_CSTRING(0); - return jsonb_from_cstring(json, strlen(json), fcinfo->context); + return jsonb_from_cstring(json, strlen(json), false, fcinfo->context); } /* @@ -100,7 +102,7 @@ jsonb_recv(PG_FUNCTION_ARGS) else elog(ERROR, "unsupported jsonb version number %d", version); - return jsonb_from_cstring(str, nbytes, NULL); + return jsonb_from_cstring(str, nbytes, false, NULL); } /* @@ -141,6 +143,18 @@ jsonb_send(PG_FUNCTION_ARGS) PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); } +/* + * Note: exported for use in SQL/JSON executor functions. + */ +Datum +jsonb_from_text(text *js, bool unique_keys) +{ + return jsonb_from_cstring(VARDATA_ANY(js), + VARSIZE_ANY_EXHDR(js), + unique_keys, + NULL); +} + /* * Get the type name of a jsonb container. */ @@ -234,7 +248,7 @@ jsonb_typeof(PG_FUNCTION_ARGS) * instead of being thrown. */ static inline Datum -jsonb_from_cstring(char *json, int len, Node *escontext) +jsonb_from_cstring(char *json, int len, bool unique_keys, Node *escontext) { JsonLexContext *lex; JsonbInState state; @@ -244,6 +258,7 @@ jsonb_from_cstring(char *json, int len, Node *escontext) memset(&sem, 0, sizeof(sem)); lex = makeJsonLexContextCstringLen(json, len, GetDatabaseEncoding(), true); + state.unique_keys = unique_keys; state.escontext = escontext; sem.semstate = (void *) &state; @@ -280,6 +295,7 @@ jsonb_in_object_start(void *pstate) JsonbInState *_state = (JsonbInState *) pstate; _state->res = pushJsonbValue(&_state->parseState, WJB_BEGIN_OBJECT, NULL); + _state->parseState->unique_keys = _state->unique_keys; return JSON_SUCCESS; } @@ -1021,6 +1037,23 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, datum_to_jsonb(val, is_null, result, tcategory, outfuncoid, key_scalar); } + +/* + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. + */ +Datum +to_jsonb_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid) +{ + JsonbInState result; + + memset(&result, 0, sizeof(JsonbInState)); + + datum_to_jsonb(val, false, &result, tcategory, outfuncoid, false); + + return JsonbPGetDatum(JsonbValueToJsonb(result.res)); +} + /* * Is the given type immutable when coming out of a JSONB context? * @@ -1072,7 +1105,6 @@ to_jsonb(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - JsonbInState result; JsonTypeCategory tcategory; Oid outfuncoid; @@ -1084,11 +1116,7 @@ to_jsonb(PG_FUNCTION_ARGS) json_categorize_type(val_type, true, &tcategory, &outfuncoid); - memset(&result, 0, sizeof(JsonbInState)); - - datum_to_jsonb(val, false, &result, tcategory, outfuncoid, false); - - PG_RETURN_POINTER(JsonbValueToJsonb(result.res)); + PG_RETURN_DATUM(to_jsonb_worker(val, tcategory, outfuncoid)); } Datum diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d3a973d86b..d1b03d6cb2 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -10832,6 +10832,15 @@ get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, case JSCTOR_JSON_ARRAY: funcname = "JSON_ARRAY"; break; + case JSCTOR_JSON_PARSE: + funcname = "JSON"; + break; + case JSCTOR_JSON_SCALAR: + funcname = "JSON_SCALAR"; + break; + case JSCTOR_JSON_SERIALIZE: + funcname = "JSON_SERIALIZE"; + break; default: elog(ERROR, "invalid JsonConstructorType %d", ctor->type); } @@ -10879,7 +10888,10 @@ get_json_constructor_options(JsonConstructorExpr *ctor, StringInfo buf) if (ctor->unique) appendStringInfoString(buf, " WITH UNIQUE KEYS"); - get_json_returning(ctor->returning, buf, true); + if (!((ctor->type == JSCTOR_JSON_PARSE || + ctor->type == JSCTOR_JSON_SCALAR) && + ctor->returning->typid == JSONOID)) + get_json_returning(ctor->returning, buf, true); } /* diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index efb5c3e098..d926713bd9 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1716,6 +1716,17 @@ typedef struct TriggerTransition /* Nodes for SQL/JSON support */ +/* + * JsonQuotes - + * representation of [KEEP|OMIT] QUOTES clause for JSON_QUERY() + */ +typedef enum JsonQuotes +{ + JS_QUOTES_UNSPEC, /* unspecified */ + JS_QUOTES_KEEP, /* KEEP QUOTES */ + JS_QUOTES_OMIT /* OMIT QUOTES */ +} JsonQuotes; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1739,6 +1750,43 @@ typedef struct JsonKeyValue JsonValueExpr *value; /* JSON value expression */ } JsonKeyValue; +/* + * JsonParseExpr - + * untransformed representation of JSON() + */ +typedef struct JsonParseExpr +{ + NodeTag type; + JsonValueExpr *expr; /* string expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + bool unique_keys; /* WITH UNIQUE KEYS? */ + int location; /* token location, or -1 if unknown */ +} JsonParseExpr; + +/* + * JsonScalarExpr - + * untransformed representation of JSON_SCALAR() + */ +typedef struct JsonScalarExpr +{ + NodeTag type; + Expr *expr; /* scalar expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonScalarExpr; + +/* + * JsonSerializeExpr - + * untransformed representation of JSON_SERIALIZE() function + */ +typedef struct JsonSerializeExpr +{ + NodeTag type; + JsonValueExpr *expr; /* json value expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonSerializeExpr; + /* * JsonObjectConstructor - * untransformed representation of JSON_OBJECT() constructor diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 0d2df069b3..85e484bf43 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1612,7 +1612,10 @@ typedef enum JsonConstructorType JSCTOR_JSON_OBJECT = 1, JSCTOR_JSON_ARRAY = 2, JSCTOR_JSON_OBJECTAGG = 3, - JSCTOR_JSON_ARRAYAGG = 4 + JSCTOR_JSON_ARRAYAGG = 4, + JSCTOR_JSON_SCALAR = 5, + JSCTOR_JSON_SERIALIZE = 6, + JSCTOR_JSON_PARSE = 7 } JsonConstructorType; /* diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index f5b2e61ca5..5984dcfa4b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -230,11 +230,13 @@ PG_KEYWORD("is", IS, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("isnull", ISNULL, TYPE_FUNC_NAME_KEYWORD, AS_LABEL) PG_KEYWORD("isolation", ISOLATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) -PG_KEYWORD("json", JSON, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index 649a1644f2..f928e6142a 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -368,7 +368,6 @@ typedef struct JsonbIterator struct JsonbIterator *parent; } JsonbIterator; - /* Convenience macros */ static inline Jsonb * DatumGetJsonbP(Datum d) @@ -418,6 +417,7 @@ extern void JsonbHashScalarValueExtended(const JsonbValue *scalarVal, uint64 *hash, uint64 seed); /* jsonb.c support functions */ +extern Datum jsonb_from_text(text *js, bool unique_keys); extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index 27a25ba283..7e9203b109 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -82,5 +82,9 @@ typedef enum void json_categorize_type(Oid typoid, bool is_jsonb, JsonTypeCategory *tcategory, Oid *outfuncoid); +extern Datum to_json_worker(Datum val, JsonTypeCategory tcategory, + Oid outfuncoid); +extern Datum to_jsonb_worker(Datum val, JsonTypeCategory tcategory, + Oid outfuncoid); #endif diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index d73c7e2c6c..d5074d73a2 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -1,3 +1,293 @@ +-- JSON() +SELECT JSON(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON(); + ^ +SELECT JSON(NULL); + json +------ + +(1 row) + +SELECT JSON('{ "a" : 1 } '); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +ERROR: JSON ENCODING clause is only allowed for bytea input type +LINE 1: SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); + ^ +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + pg_typeof +----------- + json +(1 row) + +SELECT JSON(' 1 '::json); + json +--------- + 1 +(1 row) + +SELECT JSON(' 1 '::jsonb); + json +------ + 1 +(1 row) + +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +ERROR: cannot use non-string types with WITH UNIQUE KEYS clause +LINE 1: SELECT JSON(' 1 '::json WITH UNIQUE KEYS); + ^ +SELECT JSON(123); +ERROR: cannot cast type integer to json +LINE 1: SELECT JSON(123); + ^ +SELECT JSON('{"a": 1, "a": 2}'); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +ERROR: duplicate JSON object key value +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); + QUERY PLAN +----------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); + QUERY PLAN +------------------------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON ENCODING UTF8) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); + QUERY PLAN +---------------------------------------------- + Result + Output: JSON('123'::text WITH UNIQUE KEYS) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +SELECT pg_typeof(JSON('123')); + pg_typeof +----------- + json +(1 row) + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SCALAR(); + ^ +SELECT JSON_SCALAR(NULL); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(NULL::int); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(123); + json_scalar +------------- + 123 +(1 row) + +SELECT JSON_SCALAR(123.45); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(123.45::numeric); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(true); + json_scalar +------------- + true +(1 row) + +SELECT JSON_SCALAR(false); + json_scalar +------------- + false +(1 row) + +SELECT JSON_SCALAR(' 123.45'); + json_scalar +------------- + " 123.45" +(1 row) + +SELECT JSON_SCALAR('2020-06-07'::date); + json_scalar +-------------- + "2020-06-07" +(1 row) + +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); + json_scalar +----------------------- + "2020-06-07T01:02:03" +(1 row) + +SELECT JSON_SCALAR('{}'::json); + json_scalar +------------- + {} +(1 row) + +SELECT JSON_SCALAR('{}'::jsonb); + json_scalar +------------- + {} +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); + QUERY PLAN +---------------------------- + Result + Output: JSON_SCALAR(123) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); + QUERY PLAN +------------------------------------ + Result + Output: JSON_SCALAR('123'::text) +(2 rows) + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SERIALIZE(); + ^ +SELECT JSON_SERIALIZE(NULL); + json_serialize +---------------- + +(1 row) + +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } '); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('1'); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('1' FORMAT JSON); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); + json_serialize +---------------------------- + \x7b20226122203a2031207d20 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + pg_typeof +----------- + text +(1 row) + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); +ERROR: cannot use RETURNING type jsonb in JSON_SERIALIZE() +HINT: Try returning a string type or bytea. +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); + QUERY PLAN +----------------------------------------------------- + Result + Output: JSON_SERIALIZE('{}'::json RETURNING text) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + QUERY PLAN +------------------------------------------------------ + Result + Output: JSON_SERIALIZE('{}'::json RETURNING bytea) +(2 rows) + -- JSON_OBJECT() SELECT JSON_OBJECT(); json_object @@ -630,6 +920,13 @@ ERROR: duplicate JSON object key SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); ERROR: duplicate JSON object key +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + json_objectagg +------------------ + {"1": 1, "2": 2} +(1 row) + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -645,6 +942,41 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); CREATE OR REPLACE VIEW public.json_object_view AS SELECT JSON_OBJECT('foo' : '1'::text FORMAT JSON, 'bar' : 'baz'::text RETURNING json) AS "json_object" DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + a | json_objectagg +---------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(2 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":1,"v":null} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1, "2" : 2 } + {"k":1,"v":null} | { "1" : 1, "2" : 2 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 4fd820fd51..e6e20175b0 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -1,3 +1,67 @@ +-- JSON() +SELECT JSON(); +SELECT JSON(NULL); +SELECT JSON('{ "a" : 1 } '); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + +SELECT JSON(' 1 '::json); +SELECT JSON(' 1 '::jsonb); +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +SELECT JSON(123); + +SELECT JSON('{"a": 1, "a": 2}'); +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +SELECT pg_typeof(JSON('123')); + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +SELECT JSON_SCALAR(NULL); +SELECT JSON_SCALAR(NULL::int); +SELECT JSON_SCALAR(123); +SELECT JSON_SCALAR(123.45); +SELECT JSON_SCALAR(123.45::numeric); +SELECT JSON_SCALAR(true); +SELECT JSON_SCALAR(false); +SELECT JSON_SCALAR(' 123.45'); +SELECT JSON_SCALAR('2020-06-07'::date); +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); +SELECT JSON_SCALAR('{}'::json); +SELECT JSON_SCALAR('{}'::jsonb); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +SELECT JSON_SERIALIZE(NULL); +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); +SELECT JSON_SERIALIZE('{ "a" : 1 } '); +SELECT JSON_SERIALIZE('1'); +SELECT JSON_SERIALIZE('1' FORMAT JSON); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); + + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + -- JSON_OBJECT() SELECT JSON_OBJECT(); SELECT JSON_OBJECT(RETURNING json); @@ -216,6 +280,9 @@ FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -227,6 +294,24 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index b10590a252..d251fb9a91 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1296,6 +1296,7 @@ JsonPathPredicateCallback JsonPathString JsonReturning JsonSemAction +JsonSerializeExpr JsonTokenType JsonTransformStringValuesAction JsonTypeCategory -- 2.35.3
v8-0004-JSON_TABLE.patch (application/octet-stream, 164.0K) download From 46085177985d882689fdf7b63e4fd3940d3819cd Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Tue, 18 Jul 2023 17:58:39 +0900 Subject: [PATCH v8 4/5] JSON_TABLE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This feature allows jsonb data to be treated as a table and thus used in a FROM clause like other tabular data. Data can be selected from the jsonb using jsonpath expressions, and hoisted out of nested structures in the jsonb to form multiple rows, more or less like an outer join. This also adds the PLAN clauses for JSON_TABLE, which allow the user to specify how data from nested paths are joined, allowing considerable freedom in shaping the tabular output of JSON_TABLE. PLAN DEFAULT allows the user to specify the global strategies when dealing with sibling or child nested paths. The is often sufficient to achieve the necessary goal, and is considerably simpler than the full PLAN clause, which allows the user to specify the strategy to be used for each named nested path. Author: Nikita Glukhov <n.gluhov@postgrespro.ru> Author: Teodor Sigaev <teodor@sigaev.ru> Author: Oleg Bartunov <obartunov@gmail.com> Author: Alexander Korotkov <aekorotkov@gmail.com> Author: Andrew Dunstan <andrew@dunslane.net> Author: Amit Langote <amitlangote09@gmail.com> Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- doc/src/sgml/func.sgml | 495 +++++++++ src/backend/commands/explain.c | 8 +- src/backend/executor/execExprInterp.c | 5 + src/backend/executor/nodeTableFuncscan.c | 27 +- src/backend/nodes/makefuncs.c | 19 + src/backend/nodes/nodeFuncs.c | 30 + src/backend/parser/Makefile | 1 + src/backend/parser/gram.y | 476 ++++++++- src/backend/parser/meson.build | 1 + src/backend/parser/parse_clause.c | 14 +- src/backend/parser/parse_expr.c | 35 +- src/backend/parser/parse_jsontable.c | 739 +++++++++++++ src/backend/parser/parse_relation.c | 5 +- src/backend/parser/parse_target.c | 3 + src/backend/utils/adt/jsonpath_exec.c | 543 ++++++++++ src/backend/utils/adt/ruleutils.c | 279 ++++- src/include/nodes/execnodes.h | 2 + src/include/nodes/makefuncs.h | 2 + src/include/nodes/parsenodes.h | 90 ++ src/include/nodes/primnodes.h | 61 +- src/include/parser/kwlist.h | 4 + src/include/parser/parse_clause.h | 3 + src/include/utils/jsonpath.h | 3 + src/test/regress/expected/json_sqljson.out | 6 + src/test/regress/expected/jsonb_sqljson.out | 1069 +++++++++++++++++++ src/test/regress/sql/json_sqljson.sql | 4 + src/test/regress/sql/jsonb_sqljson.sql | 629 +++++++++++ src/tools/pgindent/typedefs.list | 13 + 28 files changed, 4533 insertions(+), 33 deletions(-) create mode 100644 src/backend/parser/parse_jsontable.c diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 5dcd95f42f..04f59df1e5 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -17138,6 +17138,501 @@ array w/o UK? | t </sect2> + <sect2 id="functions-sqljson-table"> + <title>JSON_TABLE</title> + <indexterm> + <primary>json_table</primary> + </indexterm> + + <para> + <function>json_table</function> is an SQL/JSON function which + queries <acronym>JSON</acronym> data + and presents the results as a relational view, which can be accessed as a + regular SQL table. You can only use <function>json_table</function> inside the + <literal>FROM</literal> clause of a <literal>SELECT</literal> statement. + </para> + + <para> + Taking JSON data as input, <function>json_table</function> uses + a path expression to extract a part of the provided data that + will be used as a <firstterm>row pattern</firstterm> for the + constructed view. Each SQL/JSON item at the top level of the row pattern serves + as the source for a separate row in the constructed relational view. + </para> + + <para> + To split the row pattern into columns, <function>json_table</function> + provides the <literal>COLUMNS</literal> clause that defines the + schema of the created view. For each column to be constructed, + this clause provides a separate path expression that evaluates + the row pattern, extracts a JSON item, and returns it as a + separate SQL value for the specified column. If the required value + is stored in a nested level of the row pattern, it can be extracted + using the <literal>NESTED PATH</literal> subclause. Joining the + columns returned by <literal>NESTED PATH</literal> can add multiple + new rows to the constructed view. Such rows are called + <firstterm>child rows</firstterm>, as opposed to the <firstterm>parent row</firstterm> + that generates them. + </para> + + <para> + The rows produced by <function>JSON_TABLE</function> are laterally + joined to the row that generated them, so you do not have to explicitly join + the constructed view with the original table holding <acronym>JSON</acronym> + data. Optionally, you can specify how to join the columns returned + by <literal>NESTED PATH</literal> using the <literal>PLAN</literal> clause. + </para> + + <para> + Each <literal>NESTED PATH</literal> clause can generate one or more + columns. Columns produced by <literal>NESTED PATH</literal>s at the + same level are considered to be <firstterm>siblings</firstterm>, + while a column produced by a <literal>NESTED PATH</literal> is + considered to be a child of the column produced by a + <literal>NESTED PATH</literal> or row expression at a higher level. + Sibling columns are always joined first. Once they are processed, + the resulting rows are joined to the parent row. + </para> + + <para> + The syntax is: + </para> + +<synopsis> +JSON_TABLE ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> AS <replaceable>json_path_name</replaceable> </optional> <optional> PASSING { <replaceable>value</replaceable> AS <replaceable>varname</replaceable> } <optional>, ...</optional> </optional> + COLUMNS ( <replaceable class="parameter">json_table_column</replaceable> <optional>, ...</optional> ) + <optional> + PLAN ( <replaceable class="parameter">json_table_plan</replaceable> ) | + PLAN DEFAULT ( { INNER | OUTER } <optional> , { CROSS | UNION } </optional> + | { CROSS | UNION } <optional> , { INNER | OUTER } </optional> ) + </optional> +) + +<phrase> +where <replaceable class="parameter">json_table_column</replaceable> is: +</phrase> + <replaceable>name</replaceable> <replaceable>type</replaceable> <optional> PATH <replaceable>json_path_specification</replaceable> </optional> + <optional> { WITHOUT | WITH { CONDITIONAL | <optional>UNCONDITIONAL</optional> } } <optional> ARRAY </optional> WRAPPER </optional> + <optional> { KEEP | OMIT } QUOTES <optional> ON SCALAR STRING </optional> </optional> + <optional> { ERROR | NULL | DEFAULT <replaceable>expression</replaceable> } ON EMPTY </optional> + <optional> { ERROR | NULL | DEFAULT <replaceable>expression</replaceable> } ON ERROR </optional> + | <replaceable>name</replaceable> <replaceable>type</replaceable> FORMAT <replaceable>json_representation</replaceable> + <optional> PATH <replaceable>json_path_specification</replaceable> </optional> + <optional> { WITHOUT | WITH { CONDITIONAL | <optional>UNCONDITIONAL</optional> } } <optional> ARRAY </optional> WRAPPER </optional> + <optional> { KEEP | OMIT } QUOTES <optional> ON SCALAR STRING </optional> </optional> + <optional> { ERROR | NULL | EMPTY { ARRAY | OBJECT } | DEFAULT <replaceable>expression</replaceable> } ON EMPTY </optional> + <optional> { ERROR | NULL | EMPTY { ARRAY | OBJECT } | DEFAULT <replaceable>expression</replaceable> } ON ERROR </optional> + | <replaceable>name</replaceable> <replaceable>type</replaceable> EXISTS <optional> PATH <replaceable>json_path_specification</replaceable> </optional> + <optional> { ERROR | TRUE | FALSE | UNKNOWN } ON ERROR </optional> + | NESTED PATH <replaceable>json_path_specification</replaceable> <optional> AS <replaceable>path_name</replaceable> </optional> + COLUMNS ( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + | <replaceable>name</replaceable> FOR ORDINALITY +<phrase> +<replaceable>json_table_plan</replaceable> is: +</phrase> + <replaceable>json_path_name</replaceable> <optional> { OUTER | INNER } <replaceable>json_table_plan_primary</replaceable> </optional> + | <replaceable>json_table_plan_primary</replaceable> { UNION <replaceable>json_table_plan_primary</replaceable> } <optional>...</optional> + | <replaceable>json_table_plan_primary</replaceable> { CROSS <replaceable>json_table_plan_primary</replaceable> } <optional>...</optional> +<phrase> +<replaceable>json_table_plan_primary</replaceable> is: +</phrase> + <replaceable>json_path_name</replaceable> | ( <replaceable>json_table_plan</replaceable> ) +</synopsis> + + <para> + Each syntax element is described below in more detail. + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional></literal> + </term> + <listitem> + <para> + The input data to query, the JSON path expression defining the query, + and an optional <literal>PASSING</literal> clause, which can provide data + values to the <replaceable>path_expression</replaceable>. + The result of the input data + evaluation is called the <firstterm>row pattern</firstterm>. The row + pattern is used as the source for row values in the constructed view. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>COLUMNS</literal>( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + The <literal>COLUMNS</literal> clause defining the schema of the + constructed view. In this clause, you must specify all the columns + to be filled with SQL/JSON items. + The <replaceable>json_table_column</replaceable> + expression has the following syntax variants: + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>name</replaceable> <replaceable>type</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional></literal> + </term> + <listitem> + + <para> + Inserts a single SQL/JSON item into each row of + the specified column. + </para> + <para> + The provided <literal>PATH</literal> expression is evaluated and + the column is filled with the produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>ON EMPTY</literal> and + <literal>ON ERROR</literal> clauses to define how to handle missing values + or structural errors. + <literal>WRAPPER</literal> and <literal>QUOTES</literal> clauses can only + be used with JSON, array, and composite types. + These clauses have the same syntax and semantics as for + <function>json_value</function> and <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> <literal>FORMAT</literal> <replaceable>json_representation</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a composite SQL/JSON + item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression is evaluated and + the column is filled with the produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>WRAPPER</literal>, <literal>QUOTES</literal>, + <literal>ON EMPTY</literal> and <literal>ON ERROR</literal> clauses + to define additional settings for the returned SQL/JSON items. + These clauses have the same syntax and semantics as + for <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> + <literal>EXISTS</literal> <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a boolean item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression is evaluated, + a check whether any SQL/JSON items were returned is done, and + the column is filled with the resulting boolean value, one for each row. + The specified <parameter>type</parameter> should have a cast from the + <type>boolean</type>. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + </para> + <para> + Optionally, you can add <literal>ON ERROR</literal> clause to define + error behavior. This clause has the same syntax and semantics as + for <function>json_exists</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>NESTED PATH</literal> <replaceable>json_path_specification</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> + <literal>COLUMNS</literal> ( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + Extracts SQL/JSON items from nested levels of the row pattern, + generates one or more columns as defined by the <literal>COLUMNS</literal> + subclause, and inserts the extracted SQL/JSON items into each row of these columns. + The <replaceable>json_table_column</replaceable> expression in the + <literal>COLUMNS</literal> subclause uses the same syntax as in the + parent <literal>COLUMNS</literal> clause. + </para> + + <para> + The <literal>NESTED PATH</literal> syntax is recursive, + so you can go down multiple nested levels by specifying several + <literal>NESTED PATH</literal> subclauses within each other. + It allows to unnest the hierarchy of JSON objects and arrays + in a single function invocation rather than chaining several + <function>JSON_TABLE</function> expressions in an SQL statement. + </para> + + <para> + You can use the <literal>PLAN</literal> clause to define how + to join the columns returned by <literal>NESTED PATH</literal> clauses. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <literal>FOR ORDINALITY</literal> + </term> + <listitem> + + <para> + Adds an ordinality column that provides sequential row numbering. + You can have only one ordinality column per table. Row numbering + is 1-based. For child rows that result from the <literal>NESTED PATH</literal> + clauses, the parent row number is repeated. + </para> + </listitem> + </varlistentry> + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>AS</literal> <replaceable>json_path_name</replaceable> + </term> + <listitem> + + <para> + The optional <replaceable>json_path_name</replaceable> serves as an + identifier of the provided <replaceable>json_path_specification</replaceable>. + The path name must be unique and distinct from the column names. + When using the <literal>PLAN</literal> clause, you must specify the names + for all the paths, including the row pattern. Each path name can appear in + the <literal>PLAN</literal> clause only once. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN</literal> ( <replaceable>json_table_plan</replaceable> ) + </term> + <listitem> + + <para> + Defines how to join the data returned by <literal>NESTED PATH</literal> + clauses to the constructed view. + </para> + <para> + To join columns with parent/child relationship, you can use: + </para> + <variablelist> + <varlistentry> + <term> + <literal>INNER</literal> + </term> + <listitem> + + <para> + Use <literal>INNER JOIN</literal>, so that the parent row + is omitted from the output if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>OUTER</literal> + </term> + <listitem> + + <para> + Use <literal>LEFT OUTER JOIN</literal>, so that the parent row + is always included into the output even if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>, with NULL values + inserted into the child columns if the corresponding + values are missing. + </para> + <para> + This is the default option for joining columns with parent/child relationship. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para> + To join sibling columns, you can use: + </para> + + <variablelist> + <varlistentry> + <term> + <literal>UNION</literal> + </term> + <listitem> + + <para> + Generate one row for each value produced by each of the sibling + columns. The columns from the other siblings are set to null. + </para> + <para> + This is the default option for joining sibling columns. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>CROSS</literal> + </term> + <listitem> + + <para> + Generate one row for each combination of values from the sibling columns. + </para> + </listitem> + </varlistentry> + + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN DEFAULT</literal> ( <literal><replaceable>OUTER | INNER</replaceable> <optional>, <replaceable>UNION | CROSS</replaceable> </optional></literal> ) + </term> + <listitem> + <para> + The terms can also be specified in reverse order. The + <literal>INNER</literal> or <literal>OUTER</literal> option defines the + joining plan for parent/child columns, while <literal>UNION</literal> or + <literal>CROSS</literal> affects joins of sibling columns. This form + of <literal>PLAN</literal> overrides the default plan for + all columns at once. Even though the path names are not included in the + <literal>PLAN DEFAULT</literal> form, to conform to the SQL/JSON standard + they must be provided for all the paths if the <literal>PLAN</literal> + clause is used. + </para> + <para> + <literal>PLAN DEFAULT</literal> is simpler than specifying a complete + <literal>PLAN</literal>, and is often all that is required to get the desired + output. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para>Examples</para> + + <para> + In these examples the following small table storing some JSON data will be used: +<programlisting> +CREATE TABLE my_films ( js jsonb ); + +INSERT INTO my_films VALUES ( +'{ "favorites" : [ + { "kind" : "comedy", "films" : [ + { "title" : "Bananas", + "director" : "Woody Allen"}, + { "title" : "The Dinner Game", + "director" : "Francis Veber" } ] }, + { "kind" : "horror", "films" : [ + { "title" : "Psycho", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "thriller", "films" : [ + { "title" : "Vertigo", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "drama", "films" : [ + { "title" : "Yojimbo", + "director" : "Akira Kurosawa" } ] } + ] }'); +</programlisting> + </para> + <para> + Query the <structname>my_films</structname> table holding + some JSON data about the films and create a view that + distributes the film genre, title, and director between separate columns: +<screen> +SELECT jt.* FROM + my_films, + JSON_TABLE ( js, '$.favorites[*]' COLUMNS ( + id FOR ORDINALITY, + kind text PATH '$.kind', + NESTED PATH '$.films[*]' COLUMNS ( + title text PATH '$.title', + director text PATH '$.director'))) AS jt; +----+----------+------------------+------------------- + id | kind | title | director +----+----------+------------------+------------------- + 1 | comedy | Bananas | Woody Allen + 1 | comedy | The Dinner Game | Francis Veber + 2 | horror | Psycho | Alfred Hitchcock + 3 | thriller | Vertigo | Alfred Hitchcock + 4 | drama | Yojimbo | Akira Kurosawa + (5 rows) +</screen> + </para> + + <para> + Find a director that has done films in two different genres: +<screen> +SELECT + director1 AS director, title1, kind1, title2, kind2 +FROM + my_films, + JSON_TABLE ( js, '$.favorites' AS favs COLUMNS ( + NESTED PATH '$[*]' AS films1 COLUMNS ( + kind1 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film1 COLUMNS ( + title1 text PATH '$.title', + director1 text PATH '$.director') + ), + NESTED PATH '$[*]' AS films2 COLUMNS ( + kind2 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film2 COLUMNS ( + title2 text PATH '$.title', + director2 text PATH '$.director' + ) + ) + ) + PLAN (favs OUTER ((films1 INNER film1) CROSS (films2 INNER film2))) + ) AS jt + WHERE kind1 > kind2 AND director1 = director2; + + director | title1 | kind1 | title2 | kind2 +------------------+---------+----------+--------+-------- + Alfred Hitchcock | Vertigo | thriller | Psycho | horror +(1 row) +</screen> + </para> + </sect2> + <sect2 id="functions-sqljson-path"> <title>The SQL/JSON Path Language</title> diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 8570b14f62..ad899de5d6 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -3862,7 +3862,13 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es) break; case T_TableFuncScan: Assert(rte->rtekind == RTE_TABLEFUNC); - objectname = "xmltable"; + if (rte->tablefunc) + if (rte->tablefunc->functype == TFT_XMLTABLE) + objectname = "xmltable"; + else /* Must be TFT_JSON_TABLE */ + objectname = "json_table"; + else + objectname = NULL; objecttag = "Table Function Name"; break; case T_ValuesScan: diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 4c97e714ea..84c8730129 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -4309,6 +4309,11 @@ ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) break; } + case JSON_TABLE_OP: + res = item; + resnull = false; + break; + default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); *op->resnull = true; diff --git a/src/backend/executor/nodeTableFuncscan.c b/src/backend/executor/nodeTableFuncscan.c index 791cbd2372..085a612533 100644 --- a/src/backend/executor/nodeTableFuncscan.c +++ b/src/backend/executor/nodeTableFuncscan.c @@ -28,6 +28,7 @@ #include "miscadmin.h" #include "nodes/execnodes.h" #include "utils/builtins.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/xml.h" @@ -161,8 +162,9 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) scanstate->ss.ps.qual = ExecInitQual(node->scan.plan.qual, &scanstate->ss.ps); - /* Only XMLTABLE is supported currently */ - scanstate->routine = &XmlTableRoutine; + /* Only XMLTABLE and JSON_TABLE are supported currently */ + scanstate->routine = + tf->functype == TFT_XMLTABLE ? &XmlTableRoutine : &JsonbTableRoutine; scanstate->perTableCxt = AllocSetContextCreate(CurrentMemoryContext, @@ -182,6 +184,10 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) ExecInitExprList(tf->colexprs, (PlanState *) scanstate); scanstate->coldefexprs = ExecInitExprList(tf->coldefexprs, (PlanState *) scanstate); + scanstate->colvalexprs = + ExecInitExprList(tf->colvalexprs, (PlanState *) scanstate); + scanstate->passingvalexprs = + ExecInitExprList(tf->passingvalexprs, (PlanState *) scanstate); scanstate->notnulls = tf->notnulls; @@ -381,14 +387,17 @@ tfuncInitialize(TableFuncScanState *tstate, ExprContext *econtext, Datum doc) routine->SetNamespace(tstate, ns_name, ns_uri); } - /* Install the row filter expression into the table builder context */ - value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); - if (isnull) - ereport(ERROR, - (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), - errmsg("row filter expression must not be null"))); + if (routine->SetRowFilter) + { + /* Install the row filter expression into the table builder context */ + value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); + if (isnull) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("row filter expression must not be null"))); - routine->SetRowFilter(tstate, TextDatumGetCString(value)); + routine->SetRowFilter(tstate, TextDatumGetCString(value)); + } /* * Install the column filter expressions into the table builder context. diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 7bcc12b04f..7a03283713 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -878,6 +878,25 @@ makeJsonBehavior(JsonBehaviorType type, Node *default_expr) return behavior; } +/* + * makeJsonTableJoinedPlan - + * creates a joined JsonTablePlan node + */ +Node * +makeJsonTableJoinedPlan(JsonTablePlanJoinType type, Node *plan1, Node *plan2, + int location) +{ + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_JOINED; + n->join_type = type; + n->plan1 = castNode(JsonTablePlan, plan1); + n->plan2 = castNode(JsonTablePlan, plan2); + n->location = location; + + return (Node *) n; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index d31371bb4d..e07c066e15 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -2631,6 +2631,10 @@ expression_tree_walker_impl(Node *node, return true; if (WALK(tf->coldefexprs)) return true; + if (WALK(tf->colvalexprs)) + return true; + if (WALK(tf->passingvalexprs)) + return true; } break; default: @@ -3699,6 +3703,8 @@ expression_tree_mutator_impl(Node *node, MUTATE(newnode->rowexpr, tf->rowexpr, Node *); MUTATE(newnode->colexprs, tf->colexprs, List *); MUTATE(newnode->coldefexprs, tf->coldefexprs, List *); + MUTATE(newnode->colvalexprs, tf->colvalexprs, List *); + MUTATE(newnode->passingvalexprs, tf->passingvalexprs, List *); return (Node *) newnode; } break; @@ -4130,6 +4136,30 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonTable: + { + JsonTable *jt = (JsonTable *) node; + + if (WALK(jt->common)) + return true; + if (WALK(jt->columns)) + return true; + } + break; + case T_JsonTableColumn: + { + JsonTableColumn *jtc = (JsonTableColumn *) node; + + if (WALK(jtc->typeName)) + return true; + if (WALK(jtc->on_empty)) + return true; + if (WALK(jtc->on_error)) + return true; + if (jtc->coltype == JTC_NESTED && WALK(jtc->columns)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile index 9f1c4022bb..f4c0cc7f10 100644 --- a/src/backend/parser/Makefile +++ b/src/backend/parser/Makefile @@ -23,6 +23,7 @@ OBJS = \ parse_enr.o \ parse_expr.o \ parse_func.o \ + parse_jsontable.o \ parse_merge.o \ parse_node.o \ parse_oper.o \ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 0de553f5f9..6a70732b6f 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -654,19 +654,43 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_aggregate_func json_api_common_syntax json_argument + json_table + json_table_column_definition + json_table_ordinality_column_definition + json_table_regular_column_definition + json_table_formatted_column_definition + json_table_exists_column_definition + json_table_nested_columns + json_table_plan_clause_opt + json_table_plan + json_table_plan_simple + json_table_plan_parent_child + json_table_plan_outer + json_table_plan_inner + json_table_plan_sibling + json_table_plan_union + json_table_plan_cross + json_table_plan_primary %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt json_arguments + json_table_columns_clause + json_table_column_definition_list +%type <str> json_table_column_path_specification_clause_opt %type <ival> json_encoding_clause_opt json_predicate_type_constraint json_wrapper_behavior + json_table_default_plan_choices + json_table_default_plan_inner_outer + json_table_default_plan_union_cross %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt %type <jsbehavior> json_value_behavior json_query_behavior json_exists_behavior + json_table_behavior %type <js_quotes> json_quotes_clause_opt /* @@ -732,7 +756,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG - JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_TABLE JSON_VALUE KEY KEYS KEEP @@ -743,8 +767,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE - NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE - NORMALIZE NORMALIZED + NAME_P NAMES NATIONAL NATURAL NCHAR NESTED NEW NEXT NFC NFD NFKC NFKD NO + NONE NORMALIZE NORMALIZED NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC @@ -752,8 +776,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER - PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD - PLACING PLANS POLICY + PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH + PLACING PLAN PLANS POLICY POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION @@ -861,6 +885,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); * Using the same precedence as IDENT seems right for the reasons given above. */ %nonassoc UNBOUNDED /* ideally would have same precedence as IDENT */ +%nonassoc COLUMNS %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' @@ -883,6 +908,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL +%nonassoc json_table_column +%nonassoc NESTED +%left PATH %% /* @@ -13324,6 +13352,21 @@ table_ref: relation_expr opt_alias_clause $2->alias = $4; $$ = (Node *) $2; } + | json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $1); + + jt->alias = $2; + $$ = (Node *) jt; + } + | LATERAL_P json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $2); + + jt->alias = $3; + jt->lateral = true; + $$ = (Node *) jt; + } ; @@ -13891,6 +13934,8 @@ xmltable_column_option_el: { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(true), @1); } | NULL_P { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(false), @1); } + | PATH b_expr + { $$ = makeDefElem("path", $2, @1); } ; xml_namespace_list: @@ -16696,6 +16741,11 @@ json_value_behavior: | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } ; +json_table_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + ; + /* ARRAY is a noise word */ json_wrapper_behavior: WITHOUT WRAPPER { $$ = JSW_NONE; } @@ -16717,6 +16767,414 @@ json_quotes_clause_opt: | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } ; +json_table: + JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + json_table_behavior ON ERROR_P + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_columns_clause: + COLUMNS '(' json_table_column_definition_list ')' { $$ = $3; } + ; + +json_table_column_definition_list: + json_table_column_definition + { $$ = list_make1($1); } + | json_table_column_definition_list ',' json_table_column_definition + { $$ = lappend($1, $3); } + ; + +json_table_column_definition: + json_table_ordinality_column_definition %prec json_table_column + | json_table_regular_column_definition %prec json_table_column + | json_table_formatted_column_definition %prec json_table_column + | json_table_exists_column_definition %prec json_table_column + | json_table_nested_columns + ; + +json_table_ordinality_column_definition: + ColId FOR ORDINALITY + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FOR_ORDINALITY; + n->name = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_regular_column_definition: + ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_exists_column_definition: + ColId Typename + EXISTS json_table_column_path_specification_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + EXISTS json_table_column_path_specification_clause_opt + json_exists_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_column_path_specification_clause_opt: + PATH Sconst { $$ = $2; } + | /* EMPTY */ %prec json_table_column { $$ = NULL; } + ; + +json_table_formatted_column_definition: + ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->on_error = $12; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_nested_columns: + NESTED path_opt Sconst + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = NULL; + n->columns = $4; + n->location = @1; + $$ = (Node *) n; + } + | NESTED path_opt Sconst AS name + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = $5; + n->columns = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +path_opt: + PATH { } + | /* EMPTY */ { } + ; + +json_table_plan_clause_opt: + PLAN '(' json_table_plan ')' { $$ = $3; } + | PLAN DEFAULT '(' json_table_default_plan_choices ')' + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_DEFAULT; + n->join_type = $4; + n->location = @1; + $$ = (Node *) n; + } + | /* EMPTY */ { $$ = NULL; } + ; + +json_table_plan: + json_table_plan_simple + | json_table_plan_parent_child + | json_table_plan_sibling + ; + +json_table_plan_simple: + name + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_SIMPLE; + n->pathname = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_plan_primary: + json_table_plan_simple { $$ = $1; } + | '(' json_table_plan ')' + { + castNode(JsonTablePlan, $2)->location = @1; + $$ = $2; + } + ; + +json_table_plan_outer: + json_table_plan_simple OUTER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_OUTER, $1, $3, @1); } + ; + +json_table_plan_inner: + json_table_plan_simple INNER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_INNER, $1, $3, @1); } + ; + +json_table_plan_parent_child: + json_table_plan_outer + | json_table_plan_inner + ; + +json_table_plan_union: + json_table_plan_primary UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + | json_table_plan_union UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + ; + +json_table_plan_cross: + json_table_plan_primary CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + | json_table_plan_cross CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + ; + +json_table_plan_sibling: + json_table_plan_union + | json_table_plan_cross + ; + +json_table_default_plan_choices: + json_table_default_plan_inner_outer { $$ = $1 | JSTPJ_UNION; } + | json_table_default_plan_union_cross { $$ = $1 | JSTPJ_OUTER; } + | json_table_default_plan_inner_outer ',' + json_table_default_plan_union_cross { $$ = $1 | $3; } + | json_table_default_plan_union_cross ',' + json_table_default_plan_inner_outer { $$ = $1 | $3; } + ; + +json_table_default_plan_inner_outer: + INNER_P { $$ = JSTPJ_INNER; } + | OUTER_P { $$ = JSTPJ_OUTER; } + ; + +json_table_default_plan_union_cross: + UNION { $$ = JSTPJ_UNION; } + | CROSS { $$ = JSTPJ_CROSS; } + ; + json_returning_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17441,6 +17899,7 @@ unreserved_keyword: | MOVE | NAME_P | NAMES + | NESTED | NEW | NEXT | NFC @@ -17475,6 +17934,8 @@ unreserved_keyword: | PARTITION | PASSING | PASSWORD + | PATH + | PLAN | PLANS | POLICY | PRECEDING @@ -17639,6 +18100,7 @@ col_name_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | LEAST | NATIONAL @@ -18007,6 +18469,7 @@ bare_label_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | KEEP | KEY @@ -18046,6 +18509,7 @@ bare_label_keyword: | NATIONAL | NATURAL | NCHAR + | NESTED | NEW | NEXT | NFC @@ -18090,7 +18554,9 @@ bare_label_keyword: | PARTITION | PASSING | PASSWORD + | PATH | PLACING + | PLAN | PLANS | POLICY | POSITION diff --git a/src/backend/parser/meson.build b/src/backend/parser/meson.build index 94517a7f7d..0f0e4ac7ca 100644 --- a/src/backend/parser/meson.build +++ b/src/backend/parser/meson.build @@ -10,6 +10,7 @@ backend_sources += files( 'parse_enr.c', 'parse_expr.c', 'parse_func.c', + 'parse_jsontable.c', 'parse_merge.c', 'parse_node.c', 'parse_oper.c', diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index 334b9b42bd..affd812619 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -697,7 +697,11 @@ transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf) char **names; int colno; - /* Currently only XMLTABLE is supported */ + /* + * Currently we only support XMLTABLE here. See transformJsonTable() for + * JSON_TABLE support. + */ + tf->functype = TFT_XMLTABLE; constructName = "XMLTABLE"; docType = XMLOID; @@ -1104,13 +1108,17 @@ transformFromClauseItem(ParseState *pstate, Node *n, rtr->rtindex = nsitem->p_rtindex; return (Node *) rtr; } - else if (IsA(n, RangeTableFunc)) + else if (IsA(n, RangeTableFunc) || IsA(n, JsonTable)) { /* table function is like a plain relation */ RangeTblRef *rtr; ParseNamespaceItem *nsitem; - nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + if (IsA(n, RangeTableFunc)) + nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + else + nsitem = transformJsonTable(pstate, (JsonTable *) n); + *top_nsitem = nsitem; *namespace = list_make1(nsitem); rtr = makeNode(RangeTblRef); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 89343e0d6a..1c54dca905 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4227,7 +4227,7 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) JsonFormatType format; char *constructName; - if (func->common->pathname) + if (func->common->pathname && func->op != JSON_TABLE_OP) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("JSON_TABLE path name is not allowed here"), @@ -4246,6 +4246,9 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) case JSON_EXISTS_OP: constructName = "JSON_EXISTS()"; break; + case JSON_TABLE_OP: + constructName = "JSON_TABLE()"; + break; default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); break; @@ -4285,14 +4288,19 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) &jsexpr->passing_values, &jsexpr->passing_names); - if (func->op != JSON_EXISTS_OP) + if (func->op != JSON_EXISTS_OP && func->op != JSON_TABLE_OP) jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, JSON_BEHAVIOR_NULL); - jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, - func->op == JSON_EXISTS_OP ? - JSON_BEHAVIOR_FALSE : - JSON_BEHAVIOR_NULL); + if (func->op == JSON_EXISTS_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_FALSE); + else if (func->op == JSON_TABLE_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_EMPTY); + else + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_NULL); return jsexpr; } @@ -4616,6 +4624,21 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->result_coercion->expr = NULL; } break; + + case JSON_TABLE_OP: + jsexpr->returning = makeNode(JsonReturning); + jsexpr->returning->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + jsexpr->returning->typid = exprType(contextItemExpr); + jsexpr->returning->typmod = -1; + + if (jsexpr->returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("JSON_TABLE() is not yet implemented for the json type"), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + break; } if (exprType(contextItemExpr) != JSONBOID) diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c new file mode 100644 index 0000000000..a7802e0499 --- /dev/null +++ b/src/backend/parser/parse_jsontable.c @@ -0,0 +1,739 @@ +/*------------------------------------------------------------------------- + * + * parse_jsontable.c + * parsing of JSON_TABLE + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/parser/parse_jsontable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/pg_collation.h" +#include "catalog/pg_type.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "optimizer/optimizer.h" +#include "parser/parse_clause.h" +#include "parser/parse_collate.h" +#include "parser/parse_expr.h" +#include "parser/parse_relation.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/json.h" +#include "utils/lsyscache.h" + +/* Context for JSON_TABLE transformation */ +typedef struct JsonTableParseContext +{ + ParseState *pstate; /* parsing state */ + JsonTable *table; /* untransformed node */ + TableFunc *tablefunc; /* transformed node */ + List *pathNames; /* list of all path and columns names */ + int pathNameId; /* path name id counter */ + Oid contextItemTypid; /* type oid of context item (json/jsonb) */ +} JsonTableParseContext; + +static JsonTableParent *transformJsonTableColumns(JsonTableParseContext *cxt, + JsonTablePlan *plan, + List *columns, + char *pathSpec, + char **pathName, + int location); + +static Node * +makeStringConst(char *str, int location) +{ + A_Const *n = makeNode(A_Const); + + n->val.node.type = T_String; + n->val.sval.sval = str; + n->location = location; + + return (Node *) n; +} + +/* + * Transform JSON_TABLE column + * - regular column into JSON_VALUE() + * - FORMAT JSON column into JSON_QUERY() + * - EXISTS column into JSON_EXISTS() + */ +static Node * +transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, + List *passingArgs, bool errorOnError) +{ + JsonFuncExpr *jfexpr = makeNode(JsonFuncExpr); + JsonCommon *common = makeNode(JsonCommon); + JsonOutput *output = makeNode(JsonOutput); + char *pathspec; + JsonFormat *default_format; + + jfexpr->op = + jtc->coltype == JTC_REGULAR ? JSON_VALUE_OP : + jtc->coltype == JTC_EXISTS ? JSON_EXISTS_OP : JSON_QUERY_OP; + jfexpr->common = common; + jfexpr->output = output; + jfexpr->on_empty = jtc->on_empty; + jfexpr->on_error = jtc->on_error; + if (!jfexpr->on_error && errorOnError) + jfexpr->on_error = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); + jfexpr->omit_quotes = jtc->omit_quotes; + jfexpr->wrapper = jtc->wrapper; + jfexpr->location = jtc->location; + + output->typeName = jtc->typeName; + output->returning = makeNode(JsonReturning); + output->returning->format = jtc->format; + + default_format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + + common->pathname = NULL; + common->expr = makeJsonValueExpr((Expr *) contextItemExpr, default_format); + common->passing = passingArgs; + + if (jtc->pathspec) + pathspec = jtc->pathspec; + else + { + /* Construct default path as '$."column_name"' */ + StringInfoData path; + + initStringInfo(&path); + + appendStringInfoString(&path, "$."); + escape_json(&path, jtc->name); + + pathspec = path.data; + } + + common->pathspec = makeStringConst(pathspec, -1); + + return (Node *) jfexpr; +} + +static bool +isJsonTablePathNameDuplicate(JsonTableParseContext *cxt, const char *pathname) +{ + ListCell *lc; + + foreach(lc, cxt->pathNames) + { + if (!strcmp(pathname, (const char *) lfirst(lc))) + return true; + } + + return false; +} + +/* Register the column name in the path name list. */ +static void +registerJsonTableColumn(JsonTableParseContext *cxt, char *colname) +{ + if (isJsonTablePathNameDuplicate(cxt, colname)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_ALIAS), + errmsg("duplicate JSON_TABLE column name: %s", colname), + errhint("JSON_TABLE column names must be distinct from one another."))); + + cxt->pathNames = lappend(cxt->pathNames, colname); +} + +/* Recursively register all nested column names in the path name list. */ +static void +registerAllJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED) + { + if (jtc->pathname) + registerJsonTableColumn(cxt, jtc->pathname); + + registerAllJsonTableColumns(cxt, jtc->columns); + } + else + { + registerJsonTableColumn(cxt, jtc->name); + } + } +} + +/* Generate a new unique JSON_TABLE path name. */ +static char * +generateJsonTablePathName(JsonTableParseContext *cxt) +{ + char namebuf[32]; + char *name = namebuf; + + do + { + snprintf(namebuf, sizeof(namebuf), "json_table_path_%d", + ++cxt->pathNameId); + } while (isJsonTablePathNameDuplicate(cxt, name)); + + name = pstrdup(name); + cxt->pathNames = lappend(cxt->pathNames, name); + + return name; +} + +/* Collect sibling path names from plan to the specified list. */ +static void +collectSiblingPathsInJsonTablePlan(JsonTablePlan *plan, List **paths) +{ + if (plan->plan_type == JSTP_SIMPLE) + *paths = lappend(*paths, plan->pathname); + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + *paths = lappend(*paths, plan->plan1->pathname); + } + else if (plan->join_type == JSTPJ_CROSS || + plan->join_type == JSTPJ_UNION) + { + collectSiblingPathsInJsonTablePlan(plan->plan1, paths); + collectSiblingPathsInJsonTablePlan(plan->plan2, paths); + } + else + elog(ERROR, "invalid JSON_TABLE join type %d", + plan->join_type); + } +} + +/* + * Validate child JSON_TABLE plan by checking that: + * - all nested columns have path names specified + * - all nested columns have corresponding node in the sibling plan + * - plan does not contain duplicate or extra nodes + */ +static void +validateJsonTableChildPlan(ParseState *pstate, JsonTablePlan *plan, + List *columns) +{ + ListCell *lc1; + List *siblings = NIL; + int nchildren = 0; + + if (plan) + collectSiblingPathsInJsonTablePlan(plan, &siblings); + + foreach(lc1, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc1)); + + if (jtc->coltype == JTC_NESTED) + { + ListCell *lc2; + bool found = false; + + if (!jtc->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("nested JSON_TABLE columns must contain an explicit AS pathname specification if an explicit PLAN clause is used"), + parser_errposition(pstate, jtc->location))); + + /* find nested path name in the list of sibling path names */ + foreach(lc2, siblings) + { + if ((found = !strcmp(jtc->pathname, lfirst(lc2)))) + break; + } + + if (!found) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node for nested path %s was not found in plan.", jtc->pathname), + parser_errposition(pstate, jtc->location))); + + nchildren++; + } + } + + if (list_length(siblings) > nchildren) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node contains some extra or duplicate sibling nodes."), + parser_errposition(pstate, plan ? plan->location : -1))); +} + +static JsonTableColumn * +findNestedJsonTableColumn(List *columns, const char *pathname) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED && + jtc->pathname && + !strcmp(jtc->pathname, pathname)) + return jtc; + } + + return NULL; +} + +static Node * +transformNestedJsonTableColumn(JsonTableParseContext *cxt, JsonTableColumn *jtc, + JsonTablePlan *plan) +{ + JsonTableParent *node; + char *pathname = jtc->pathname; + + node = transformJsonTableColumns(cxt, plan, jtc->columns, jtc->pathspec, + &pathname, jtc->location); + + return (Node *) node; +} + +static Node * +makeJsonTableSiblingJoin(bool cross, Node *lnode, Node *rnode) +{ + JsonTableSibling *join = makeNode(JsonTableSibling); + + join->larg = lnode; + join->rarg = rnode; + join->cross = cross; + + return (Node *) join; +} + +/* + * Recursively transform child JSON_TABLE plan. + * + * Default plan is transformed into a cross/union join of its nested columns. + * Simple and outer/inner plans are transformed into a JsonTableParent by + * finding and transforming corresponding nested column. + * Sibling plans are recursively transformed into a JsonTableSibling. + */ +static Node * +transformJsonTableChildPlan(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns) +{ + JsonTableColumn *jtc = NULL; + + if (!plan || plan->plan_type == JSTP_DEFAULT) + { + /* unspecified or default plan */ + Node *res = NULL; + ListCell *lc; + bool cross = plan && (plan->join_type & JSTPJ_CROSS); + + /* transform all nested columns into cross/union join */ + foreach(lc, columns) + { + JsonTableColumn *col = castNode(JsonTableColumn, lfirst(lc)); + Node *node; + + if (col->coltype != JTC_NESTED) + continue; + + node = transformNestedJsonTableColumn(cxt, col, plan); + + /* join transformed node with previous sibling nodes */ + res = res ? makeJsonTableSiblingJoin(cross, res, node) : node; + } + + return res; + } + else if (plan->plan_type == JSTP_SIMPLE) + { + jtc = findNestedJsonTableColumn(columns, plan->pathname); + } + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + jtc = findNestedJsonTableColumn(columns, plan->plan1->pathname); + } + else + { + Node *node1 = transformJsonTableChildPlan(cxt, plan->plan1, + columns); + Node *node2 = transformJsonTableChildPlan(cxt, plan->plan2, + columns); + + return makeJsonTableSiblingJoin(plan->join_type == JSTPJ_CROSS, + node1, node2); + } + } + else + elog(ERROR, "invalid JSON_TABLE plan type %d", plan->plan_type); + + if (!jtc) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name was %s not found in nested columns list.", + plan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + return transformNestedJsonTableColumn(cxt, jtc, plan); +} + +/* Check whether type is json/jsonb, array, or record. */ +static bool +typeIsComposite(Oid typid) +{ + char typtype; + + if (typid == JSONOID || + typid == JSONBOID || + typid == RECORDOID || + type_is_array(typid)) + return true; + + typtype = get_typtype(typid); + + if (typtype == TYPTYPE_COMPOSITE) + return true; + + if (typtype == TYPTYPE_DOMAIN) + return typeIsComposite(getBaseType(typid)); + + return false; +} + +/* Append transformed non-nested JSON_TABLE columns to the TableFunc node */ +static void +appendJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *col; + ParseState *pstate = cxt->pstate; + JsonTable *jt = cxt->table; + TableFunc *tf = cxt->tablefunc; + bool errorOnError = jt->on_error && + jt->on_error->btype == JSON_BEHAVIOR_ERROR; + + foreach(col, columns) + { + JsonTableColumn *rawc = castNode(JsonTableColumn, lfirst(col)); + Oid typid; + int32 typmod; + Node *colexpr; + + if (rawc->name) + { + /* make sure column names are unique */ + ListCell *colname; + + foreach(colname, tf->colnames) + if (!strcmp((const char *) colname, rawc->name)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("column name \"%s\" is not unique", + rawc->name), + parser_errposition(pstate, rawc->location))); + + tf->colnames = lappend(tf->colnames, + makeString(pstrdup(rawc->name))); + } + + /* + * Determine the type and typmod for the new column. FOR ORDINALITY + * columns are INTEGER by standard; the others are user-specified. + */ + switch (rawc->coltype) + { + case JTC_FOR_ORDINALITY: + colexpr = NULL; + typid = INT4OID; + typmod = -1; + break; + + case JTC_REGULAR: + typenameTypeIdAndMod(pstate, rawc->typeName, &typid, &typmod); + + /* + * Use implicit FORMAT JSON for composite types (arrays and + * records) + */ + if (typeIsComposite(typid)) + rawc->coltype = JTC_FORMATTED; + else if (rawc->wrapper != JSW_NONE) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use WITH WRAPPER clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + else if (rawc->omit_quotes) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use OMIT QUOTES clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + + /* FALLTHROUGH */ + case JTC_EXISTS: + case JTC_FORMATTED: + { + Node *je; + CaseTestExpr *param = makeNode(CaseTestExpr); + + param->collation = InvalidOid; + param->typeId = cxt->contextItemTypid; + param->typeMod = -1; + + je = transformJsonTableColumn(rawc, (Node *) param, + NIL, errorOnError); + + colexpr = transformExpr(pstate, je, EXPR_KIND_FROM_FUNCTION); + assign_expr_collations(pstate, colexpr); + + typid = exprType(colexpr); + typmod = exprTypmod(colexpr); + break; + } + + case JTC_NESTED: + continue; + + default: + elog(ERROR, "unknown JSON_TABLE column type: %d", rawc->coltype); + break; + } + + tf->coltypes = lappend_oid(tf->coltypes, typid); + tf->coltypmods = lappend_int(tf->coltypmods, typmod); + tf->colcollations = lappend_oid(tf->colcollations, get_typcollation(typid)); + tf->colvalexprs = lappend(tf->colvalexprs, colexpr); + } +} + +/* + * Create transformed JSON_TABLE parent plan node by appending all non-nested + * columns to the TableFunc node and remembering their indices in the + * colvalexprs list. + */ +static JsonTableParent * +makeParentJsonTableNode(JsonTableParseContext *cxt, char *pathSpec, char *pathName, + List *columns) +{ + JsonTableParent *node = makeNode(JsonTableParent); + + node->path = makeNode(JsonTablePath); + node->path->value = makeConst(JSONPATHOID, -1, InvalidOid, -1, + DirectFunctionCall1(jsonpath_in, + CStringGetDatum(pathSpec)), + false, false); + if (pathName) + node->path->name = pstrdup(pathName); + + /* save start of column range */ + node->colMin = list_length(cxt->tablefunc->colvalexprs); + + appendJsonTableColumns(cxt, columns); + + /* save end of column range */ + node->colMax = list_length(cxt->tablefunc->colvalexprs) - 1; + + node->errorOnError = + cxt->table->on_error && + cxt->table->on_error->btype == JSON_BEHAVIOR_ERROR; + + return node; +} + +static JsonTableParent * +transformJsonTableColumns(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns, char *pathSpec, char **pathName, + int location) +{ + JsonTableParent *node; + JsonTablePlan *childPlan; + bool defaultPlan = !plan || plan->plan_type == JSTP_DEFAULT; + + if (!*pathName) + { + if (cxt->table->plan) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE expression"), + errdetail("JSON_TABLE columns must contain " + "explicit AS pathname specification if " + "explicit PLAN clause is used"), + parser_errposition(cxt->pstate, location))); + + *pathName = generateJsonTablePathName(cxt); + } + + if (defaultPlan) + childPlan = plan; + else + { + /* validate parent and child plans */ + JsonTablePlan *parentPlan; + + if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type != JSTPJ_INNER && + plan->join_type != JSTPJ_OUTER) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Expected INNER or OUTER JSON_TABLE plan node."), + parser_errposition(cxt->pstate, plan->location))); + + parentPlan = plan->plan1; + childPlan = plan->plan2; + + Assert(parentPlan->plan_type != JSTP_JOINED); + Assert(parentPlan->pathname); + } + else + { + parentPlan = plan; + childPlan = NULL; + } + + if (strcmp(parentPlan->pathname, *pathName)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name mismatch: expected %s but %s is given.", + *pathName, parentPlan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + validateJsonTableChildPlan(cxt->pstate, childPlan, columns); + } + + /* transform only non-nested columns */ + node = makeParentJsonTableNode(cxt, pathSpec, *pathName, columns); + + if (childPlan || defaultPlan) + { + /* transform recursively nested columns */ + node->child = transformJsonTableChildPlan(cxt, childPlan, columns); + if (node->child) + node->outerJoin = !plan || (plan->join_type & JSTPJ_OUTER); + /* else: default plan case, no children found */ + } + + return node; +} + +/* + * transformJsonTable - + * Transform a raw JsonTable into TableFunc. + * + * Transform the document-generating expression, the row-generating expression, + * the column-generating expressions, and the default value expressions. + */ +ParseNamespaceItem * +transformJsonTable(ParseState *pstate, JsonTable *jt) +{ + JsonTableParseContext cxt; + TableFunc *tf = makeNode(TableFunc); + JsonFuncExpr *jfe = makeNode(JsonFuncExpr); + JsonExpr *je; + JsonTablePlan *plan = jt->plan; + JsonCommon *jscommon; + char *rootPathName = jt->common->pathname; + char *rootPath; + bool is_lateral; + + cxt.pstate = pstate; + cxt.table = jt; + cxt.tablefunc = tf; + cxt.pathNames = NIL; + cxt.pathNameId = 0; + + if (rootPathName) + registerJsonTableColumn(&cxt, rootPathName); + + registerAllJsonTableColumns(&cxt, jt->columns); + +#if 0 /* XXX it' unclear from the standard whether + * root path name is mandatory or not */ + if (plan && plan->plan_type != JSTP_DEFAULT && !rootPathName) + { + /* Assign root path name and create corresponding plan node */ + JsonTablePlan *rootNode = makeNode(JsonTablePlan); + JsonTablePlan *rootPlan = (JsonTablePlan *) + makeJsonTableJoinedPlan(JSTPJ_OUTER, (Node *) rootNode, + (Node *) plan, jt->location); + + rootPathName = generateJsonTablePathName(&cxt); + + rootNode->plan_type = JSTP_SIMPLE; + rootNode->pathname = rootPathName; + + plan = rootPlan; + } +#endif + + jscommon = copyObject(jt->common); + jscommon->pathspec = makeStringConst(pstrdup("$"), -1); + + jfe->op = JSON_TABLE_OP; + jfe->common = jscommon; + jfe->on_error = jt->on_error; + jfe->location = jt->common->location; + + /* + * We make lateral_only names of this level visible, whether or not the + * RangeTableFunc is explicitly marked LATERAL. This is needed for SQL + * spec compliance and seems useful on convenience grounds for all + * functions in FROM. + * + * (LATERAL can't nest within a single pstate level, so we don't need + * save/restore logic here.) + */ + Assert(!pstate->p_lateral_active); + pstate->p_lateral_active = true; + + tf->functype = TFT_JSON_TABLE; + tf->docexpr = transformExpr(pstate, (Node *) jfe, EXPR_KIND_FROM_FUNCTION); + + cxt.contextItemTypid = exprType(tf->docexpr); + + if (!IsA(jt->common->pathspec, A_Const) || + castNode(A_Const, jt->common->pathspec)->val.node.type != T_String) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("only string constants supported in JSON_TABLE path specification"), + parser_errposition(pstate, + exprLocation(jt->common->pathspec)))); + + rootPath = castNode(A_Const, jt->common->pathspec)->val.sval.sval; + + tf->plan = (Node *) transformJsonTableColumns(&cxt, plan, jt->columns, + rootPath, &rootPathName, + jt->common->location); + + /* Also save a copy of the PASSING arguments in the TableFunc node. */ + je = (JsonExpr *) tf->docexpr; + tf->passingvalexprs = copyObject(je->passing_values); + + tf->ordinalitycol = -1; /* undefine ordinality column number */ + tf->location = jt->location; + + pstate->p_lateral_active = false; + + /* + * Mark the RTE as LATERAL if the user said LATERAL explicitly, or if + * there are any lateral cross-references in it. + */ + is_lateral = jt->lateral || contain_vars_of_level((Node *) tf, 0); + + return addRangeTableEntryForTableFunc(pstate, + tf, jt->alias, is_lateral, true); +} diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 864ea9b0d5..3ac7e113f0 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -2073,7 +2073,8 @@ addRangeTableEntryForTableFunc(ParseState *pstate, Assert(list_length(tf->coltypmods) == list_length(tf->colnames)); Assert(list_length(tf->colcollations) == list_length(tf->colnames)); - refname = alias ? alias->aliasname : pstrdup("xmltable"); + refname = alias ? alias->aliasname : + pstrdup(tf->functype == TFT_XMLTABLE ? "xmltable" : "json_table"); rte->rtekind = RTE_TABLEFUNC; rte->relid = InvalidOid; @@ -2096,7 +2097,7 @@ addRangeTableEntryForTableFunc(ParseState *pstate, ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), errmsg("%s function has %d columns available but %d columns specified", - "XMLTABLE", + tf->functype == TFT_XMLTABLE ? "XMLTABLE" : "JSON_TABLE", list_length(tf->colnames), numaliases))); rte->eref = eref; diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4f94fc69d6..6500e42485 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1992,6 +1992,9 @@ FigureColnameInternal(Node *node, char **name) case JSON_EXISTS_OP: *name = "json_exists"; return 2; + case JSON_TABLE_OP: + *name = "json_table"; + return 2; } break; default: diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index eded22e194..92d76d5cde 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -61,10 +61,12 @@ #include "catalog/pg_collation.h" #include "catalog/pg_type.h" +#include "executor/execExpr.h" #include "funcapi.h" #include "lib/stringinfo.h" #include "miscadmin.h" #include "nodes/miscnodes.h" +#include "nodes/nodeFuncs.h" #include "regex/regex.h" #include "utils/builtins.h" #include "utils/date.h" @@ -75,6 +77,8 @@ #include "utils/guc.h" #include "utils/json.h" #include "utils/jsonpath.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" #include "utils/timestamp.h" #include "utils/varlena.h" @@ -156,6 +160,61 @@ typedef struct JsonValueListIterator ListCell *next; } JsonValueListIterator; +/* Structures for JSON_TABLE execution */ + +typedef enum JsonTablePlanStateType +{ + JSON_TABLE_SCAN_STATE = 0, + JSON_TABLE_JOIN_STATE +} JsonTablePlanStateType; + +typedef struct JsonTablePlanState +{ + JsonTablePlanStateType type; + + struct JsonTablePlanState *parent; + struct JsonTablePlanState *nested; +} JsonTablePlanState; + +typedef struct JsonTableScanState +{ + JsonTablePlanState plan; + + MemoryContext mcxt; + JsonPath *path; + List *args; + JsonValueList found; + JsonValueListIterator iter; + Datum current; + int ordinal; + bool currentIsNull; + bool outerJoin; + bool errorOnError; + bool advanceNested; + bool reset; +} JsonTableScanState; + +typedef struct JsonTableJoinState +{ + JsonTablePlanState plan; + + JsonTablePlanState *left; + JsonTablePlanState *right; + bool cross; + bool advanceRight; +} JsonTableJoinState; + +/* random number to identify JsonTableExecContext */ +#define JSON_TABLE_EXEC_CONTEXT_MAGIC 418352867 + +typedef struct JsonTableExecContext +{ + int magic; + JsonTableScanState **colexprscans; + JsonTableScanState *root; + bool empty; +} JsonTableExecContext; + /* strict/lax flags is decomposed into four [un]wrap/error flags */ #define jspStrictAbsenseOfErrors(cxt) (!(cxt)->laxMode) #define jspAutoUnwrap(cxt) ((cxt)->laxMode) @@ -248,6 +307,7 @@ static JsonPathExecResult getArrayIndex(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbValue *jb, int32 *index); static JsonBaseObjectInfo setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id); +static void JsonValueListClear(JsonValueList *jvl); static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv); static int JsonValueListLength(const JsonValueList *jvl); static bool JsonValueListIsEmpty(JsonValueList *jvl); @@ -265,6 +325,14 @@ static JsonbValue *wrapItemsInArray(const JsonValueList *items); static int compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, bool useTz, bool *cast_error); + +static JsonTablePlanState *JsonTableInitPlanState(JsonTableExecContext *cxt, + Node *plan, + JsonTablePlanState *parent); +static bool JsonTablePlanNextRow(JsonTablePlanState *state); +static bool JsonTableScanNextRow(JsonTableScanState *scan); + + /****************** User interface to JsonPath executor ********************/ /* @@ -2518,6 +2586,13 @@ setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id) return baseObject; } +static void +JsonValueListClear(JsonValueList *jvl) +{ + jvl->singleton = NULL; + jvl->list = NULL; +} + static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv) { @@ -3123,3 +3198,471 @@ JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); } } + +/************************ JSON_TABLE functions ***************************/ + +/* + * Returns private data from executor state. Ensure validity by check with + * MAGIC number. + */ +static inline JsonTableExecContext * +GetJsonTableExecContext(TableFuncScanState *state, const char *fname) +{ + JsonTableExecContext *result; + + if (!IsA(state, TableFuncScanState)) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + result = (JsonTableExecContext *) state->opaque; + if (result->magic != JSON_TABLE_EXEC_CONTEXT_MAGIC) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + + return result; +} + +/* Recursively initialize JSON_TABLE scan / join state */ +static JsonTableJoinState * +JsonTableInitJoinState(JsonTableExecContext *cxt, JsonTableSibling *plan, + JsonTablePlanState *parent) +{ + JsonTableJoinState *join = palloc0(sizeof(*join)); + + join->plan.type = JSON_TABLE_JOIN_STATE; + /* parent and nested not set. */ + + join->cross = plan->cross; + join->left = JsonTableInitPlanState(cxt, plan->larg, parent); + join->right = JsonTableInitPlanState(cxt, plan->rarg, parent); + + return join; +} + +static JsonTableScanState * +JsonTableInitScanState(JsonTableExecContext *cxt, JsonTableParent *plan, + JsonTablePlanState *parent, + List *args, MemoryContext mcxt) +{ + JsonTableScanState *scan = palloc0(sizeof(*scan)); + int i; + + scan->plan.type = JSON_TABLE_SCAN_STATE; + scan->plan.parent = parent; + + scan->outerJoin = plan->outerJoin; + scan->errorOnError = plan->errorOnError; + scan->path = DatumGetJsonPathP(plan->path->value->constvalue); + scan->args = args; + scan->mcxt = AllocSetContextCreate(mcxt, "JsonTableExecContext", + ALLOCSET_DEFAULT_SIZES); + + /* + * Set after settings scan->args and scan->mcxt, because the recursive call + * wants to use those values. + */ + scan->plan.nested = plan->child ? + JsonTableInitPlanState(cxt, plan->child, (JsonTablePlanState *) scan) : + NULL; + + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + + for (i = plan->colMin; i <= plan->colMax; i++) + cxt->colexprscans[i] = scan; + + return scan; +} + +/* Recursively initialize JSON_TABLE scan state */ +static JsonTablePlanState * +JsonTableInitPlanState(JsonTableExecContext *cxt, Node *plan, + JsonTablePlanState *parent) +{ + JsonTablePlanState *state; + + if (IsA(plan, JsonTableSibling)) + { + JsonTableSibling *join = castNode(JsonTableSibling, plan); + + state = (JsonTablePlanState *) + JsonTableInitJoinState(cxt, join, parent); + } + else + { + JsonTableParent *scan = castNode(JsonTableParent, plan); + JsonTableScanState *parent_scan = (JsonTableScanState *) parent; + + Assert(parent_scan); + state = (JsonTablePlanState *) + JsonTableInitScanState(cxt, scan, parent, parent_scan->args, + parent_scan->mcxt); + } + + return state; +} + +/* + * JsonTableInitOpaque + * Fill in TableFuncScanState->opaque for JsonTable processor + */ +static void +JsonTableInitOpaque(TableFuncScanState *state, int natts) +{ + JsonTableExecContext *cxt; + PlanState *ps = &state->ss.ps; + TableFuncScan *tfs = castNode(TableFuncScan, ps->plan); + TableFunc *tf = tfs->tablefunc; + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + JsonExpr *je = castNode(JsonExpr, tf->docexpr); + List *args = NIL; + + cxt = palloc0(sizeof(JsonTableExecContext)); + cxt->magic = JSON_TABLE_EXEC_CONTEXT_MAGIC; + + if (state->passingvalexprs) + { + ListCell *exprlc; + ListCell *namelc; + + Assert(list_length(state->passingvalexprs) == + list_length(je->passing_names)); + forboth(exprlc, state->passingvalexprs, + namelc, je->passing_names) + { + ExprState *state = lfirst_node(ExprState, exprlc); + String *name = lfirst_node(String, namelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(name->sval); + var->typid = exprType((Node *) state->expr); + var->typmod = exprTypmod((Node *) state->expr); + + /* + * Evaluate the expression and save the value to be returned by + * GetJsonPathVar(). + */ + var->value = ExecEvalExpr(state, ps->ps_ExprContext, + &var->isnull); + + args = lappend(args, var); + } + } + + cxt->colexprscans = palloc(sizeof(JsonTableScanState *) * + list_length(tf->colvalexprs)); + + cxt->root = JsonTableInitScanState(cxt, root, NULL, args, + CurrentMemoryContext); + state->opaque = cxt; +} + +/* Reset scan iterator to the beginning of the item list */ +static void +JsonTableRescan(JsonTableScanState *scan) +{ + JsonValueListInitIterator(&scan->found, &scan->iter); + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + scan->advanceNested = false; + scan->ordinal = 0; +} + +/* Reset context item of a scan, execute JSON path and reset a scan */ +static void +JsonTableResetContextItem(JsonTableScanState *scan, Datum item) +{ + MemoryContext oldcxt; + JsonPathExecResult res; + Jsonb *js = (Jsonb *) DatumGetJsonbP(item); + + JsonValueListClear(&scan->found); + + MemoryContextResetOnly(scan->mcxt); + + oldcxt = MemoryContextSwitchTo(scan->mcxt); + + res = executeJsonPath(scan->path, scan->args, GetJsonPathVar, js, + scan->errorOnError, &scan->found, + false /* FIXME */ ); + + MemoryContextSwitchTo(oldcxt); + + if (jperIsError(res)) + { + Assert(!scan->errorOnError); + JsonValueListClear(&scan->found); /* EMPTY ON ERROR case */ + } + + JsonTableRescan(scan); +} + +/* + * JsonTableSetDocument + * Install the input document + */ +static void +JsonTableSetDocument(TableFuncScanState *state, Datum value) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableSetDocument"); + + JsonTableResetContextItem(cxt->root, value); +} + +/* Recursively reset scan and its child nodes */ +static void +JsonTableRescanRecursive(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTableRescanRecursive(join->left); + JsonTableRescanRecursive(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan = (JsonTableScanState *) state; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + JsonTableRescan(scan); + if (scan->plan.nested) + JsonTableRescanRecursive(scan->plan.nested); + } +} + +/* + * Fetch next row from a cross/union joined scan. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTablePlanNextRow(JsonTablePlanState *state) +{ + JsonTableJoinState *join; + + if (state->type == JSON_TABLE_SCAN_STATE) + return JsonTableScanNextRow((JsonTableScanState *) state); + + join = (JsonTableJoinState *) state; + if (join->advanceRight) + { + /* fetch next inner row */ + if (JsonTablePlanNextRow(join->right)) + return true; + + /* inner rows are exhausted */ + if (join->cross) + join->advanceRight = false; /* next outer row */ + else + return false; /* end of scan */ + } + + while (!join->advanceRight) + { + /* fetch next outer row */ + bool left = JsonTablePlanNextRow(join->left); + + if (join->cross) + { + if (!left) + return false; /* end of scan */ + + JsonTableRescanRecursive(join->right); + + if (!JsonTablePlanNextRow(join->right)) + continue; /* next outer row */ + + join->advanceRight = true; /* next inner row */ + } + else if (!left) + { + if (!JsonTablePlanNextRow(join->right)) + return false; /* end of scan */ + + join->advanceRight = true; /* next inner row */ + } + + break; + } + + return true; +} + +/* Recursively set 'reset' flag of scan and its child nodes */ +static void +JsonTablePlanReset(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTablePlanReset(join->left); + JsonTablePlanReset(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + scan = (JsonTableScanState *) state; + scan->reset = true; + scan->advanceNested = false; + + if (scan->plan.nested) + JsonTablePlanReset(scan->plan.nested); + } +} + +/* + * Fetch next row from a simple scan with outer/inner joined nested subscans. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTableScanNextRow(JsonTableScanState *scan) +{ + /* reset context item if requested */ + if (scan->reset) + { + JsonTableScanState *parent_scan = + (JsonTableScanState *) scan->plan.parent; + + Assert(parent_scan && !parent_scan->currentIsNull); + JsonTableResetContextItem(scan, parent_scan->current); + scan->reset = false; + } + + if (scan->advanceNested) + { + /* fetch next nested row */ + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested) + return true; + } + + for (;;) + { + /* fetch next row */ + JsonbValue *jbv = JsonValueListNext(&scan->found, &scan->iter); + MemoryContext oldcxt; + + if (!jbv) + { + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + return false; /* end of scan */ + } + + /* set current row item */ + oldcxt = MemoryContextSwitchTo(scan->mcxt); + scan->current = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + scan->currentIsNull = false; + MemoryContextSwitchTo(oldcxt); + + scan->ordinal++; + + if (!scan->plan.nested) + break; + + JsonTablePlanReset(scan->plan.nested); + + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested || scan->outerJoin) + break; + } + + return true; +} + +/* + * JsonTableFetchRow + * Prepare the next "current" tuple for upcoming GetValue calls. + * Returns FALSE if the row-filter expression returned no more rows. + */ +static bool +JsonTableFetchRow(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableFetchRow"); + + if (cxt->empty) + return false; + + return JsonTableScanNextRow(cxt->root); +} + +/* + * JsonTableGetValue + * Return the value for column number 'colnum' for the current row. + * + * This leaks memory, so be sure to reset often the context in which it's + * called. + */ +static Datum +JsonTableGetValue(TableFuncScanState *state, int colnum, + Oid typid, int32 typmod, bool *isnull) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableGetValue"); + ExprContext *econtext = state->ss.ps.ps_ExprContext; + ExprState *estate = list_nth(state->colvalexprs, colnum); + JsonTableScanState *scan = cxt->colexprscans[colnum]; + Datum result; + + if (scan->currentIsNull) /* NULL from outer/union join */ + { + result = (Datum) 0; + *isnull = true; + } + else if (estate) /* regular column */ + { + Datum saved_caseValue = econtext->caseValue_datum; + bool saved_caseIsNull = econtext->caseValue_isNull; + + /* Pass the value for CaseTestExpr that may be present in colexpr */ + econtext->caseValue_datum = scan->current; + econtext->caseValue_isNull = false; + + result = ExecEvalExpr(estate, econtext, isnull); + + econtext->caseValue_datum = saved_caseValue; + econtext->caseValue_isNull = saved_caseIsNull; + } + else + { + result = Int32GetDatum(scan->ordinal); /* ordinality column */ + *isnull = false; + } + + return result; +} + +/* + * JsonTableDestroyOpaque + */ +static void +JsonTableDestroyOpaque(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableDestroyOpaque"); + + /* not valid anymore */ + cxt->magic = 0; + + state->opaque = NULL; +} + +const TableFuncRoutine JsonbTableRoutine = +{ + JsonTableInitOpaque, + JsonTableSetDocument, + NULL, + NULL, + NULL, + JsonTableFetchRow, + JsonTableGetValue, + JsonTableDestroyOpaque +}; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d1ec418ccf..e89d1e03d6 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -522,6 +522,8 @@ static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); static void get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit); +static void get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8606,7 +8608,8 @@ get_json_behavior(JsonBehavior *behavior, deparse_context *context, /* * get_json_expr_options * - * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS and + * JSON_TABLE columns. */ static void get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, @@ -9854,6 +9857,9 @@ get_rule_expr(Node *node, deparse_context *context, case JSON_EXISTS_OP: appendStringInfoString(buf, "JSON_EXISTS("); break; + default: + elog(ERROR, "unexpected JsonExpr type: %d", jexpr->op); + break; } get_rule_expr(jexpr->formatted_expr, context, showimplicit); @@ -11207,16 +11213,14 @@ get_sublink_expr(SubLink *sublink, deparse_context *context) /* ---------- - * get_tablefunc - Parse back a table function + * get_xmltable - Parse back a XMLTABLE function * ---------- */ static void -get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +get_xmltable(TableFunc *tf, deparse_context *context, bool showimplicit) { StringInfo buf = context->buf; - /* XMLTABLE is the only existing implementation. */ - appendStringInfoString(buf, "XMLTABLE("); if (tf->ns_uris != NIL) @@ -11307,6 +11311,271 @@ get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) appendStringInfoChar(buf, ')'); } +/* + * get_json_nested_columns - Parse back nested JSON_TABLE columns + */ +static void +get_json_table_nested_columns(TableFunc *tf, Node *node, + deparse_context *context, bool showimplicit, + bool needcomma) +{ + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_nested_columns(tf, n->larg, context, showimplicit, + needcomma); + get_json_table_nested_columns(tf, n->rarg, context, showimplicit, true); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + if (needcomma) + appendStringInfoChar(context->buf, ','); + + appendStringInfoChar(context->buf, ' '); + appendContextKeyword(context, "NESTED PATH ", 0, 0, 0); + get_const_expr(n->path->value, context, -1); + appendStringInfo(context->buf, " AS %s", quote_identifier(n->path->name)); + get_json_table_columns(tf, n, context, showimplicit); + } +} + +/* + * get_json_table_plan - Parse back a JSON_TABLE plan + */ +static void +get_json_table_plan(TableFunc *tf, Node *node, deparse_context *context, + bool parenthesize) +{ + if (parenthesize) + appendStringInfoChar(context->buf, '('); + + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_plan(tf, n->larg, context, + IsA(n->larg, JsonTableSibling) || + castNode(JsonTableParent, n->larg)->child); + + appendStringInfoString(context->buf, n->cross ? " CROSS " : " UNION "); + + get_json_table_plan(tf, n->rarg, context, + IsA(n->rarg, JsonTableSibling) || + castNode(JsonTableParent, n->rarg)->child); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + appendStringInfoString(context->buf, quote_identifier(n->path->name)); + + if (n->child) + { + appendStringInfoString(context->buf, + n->outerJoin ? " OUTER " : " INNER "); + get_json_table_plan(tf, n->child, context, + IsA(n->child, JsonTableSibling)); + } + } + + if (parenthesize) + appendStringInfoChar(context->buf, ')'); +} + +/* + * get_json_table_columns - Parse back JSON_TABLE columns + */ +static void +get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + ListCell *lc_colname; + ListCell *lc_coltype; + ListCell *lc_coltypmod; + ListCell *lc_colvalexpr; + int colnum = 0; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "COLUMNS (", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forfour(lc_colname, tf->colnames, + lc_coltype, tf->coltypes, + lc_coltypmod, tf->coltypmods, + lc_colvalexpr, tf->colvalexprs) + { + char *colname = strVal(lfirst(lc_colname)); + JsonExpr *colexpr; + Oid typid; + int32 typmod; + bool ordinality; + JsonBehaviorType default_behavior; + + typid = lfirst_oid(lc_coltype); + typmod = lfirst_int(lc_coltypmod); + colexpr = castNode(JsonExpr, lfirst(lc_colvalexpr)); + + if (colnum < node->colMin) + { + colnum++; + continue; + } + + if (colnum > node->colMax) + break; + + if (colnum > node->colMin) + appendStringInfoString(buf, ", "); + + colnum++; + + ordinality = !colexpr; + + appendContextKeyword(context, "", 0, 0, 0); + + appendStringInfo(buf, "%s %s", quote_identifier(colname), + ordinality ? "FOR ORDINALITY" : + format_type_with_typemod(typid, typmod)); + if (ordinality) + continue; + + if (colexpr->op == JSON_EXISTS_OP) + { + appendStringInfoString(buf, " EXISTS"); + default_behavior = JSON_BEHAVIOR_FALSE; + } + else + { + if (colexpr->op == JSON_QUERY_OP) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(typid, &typcategory, &typispreferred); + + if (typcategory == TYPCATEGORY_STRING) + appendStringInfoString(buf, + colexpr->format->format_type == JS_FORMAT_JSONB ? + " FORMAT JSONB" : " FORMAT JSON"); + } + + default_behavior = JSON_BEHAVIOR_NULL; + } + + if (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR) + default_behavior = JSON_BEHAVIOR_ERROR; + + appendStringInfoString(buf, " PATH "); + + get_json_path_spec(colexpr->path_spec, context, showimplicit); + + get_json_expr_options(colexpr, context, default_behavior); + } + + if (node->child) + get_json_table_nested_columns(tf, node->child, context, showimplicit, + node->colMax >= node->colMin); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_json_table - Parse back a JSON_TABLE function + * ---------- + */ +static void +get_json_table(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + + appendStringInfoString(buf, "JSON_TABLE("); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_const_expr(root->path->value, context, -1); + + appendStringInfo(buf, " AS %s", quote_identifier(root->path->name)); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PASSING ", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr((Node *) lfirst(lc2), context, false); + appendStringInfo(buf, " AS %s", + quote_identifier((lfirst_node(String, lc1))->sval) + ); + } + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + } + + get_json_table_columns(tf, root, context, showimplicit); + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PLAN ", 0, 0, 0); + get_json_table_plan(tf, (Node *) root, context, true); + + if (jexpr->on_error->btype != JSON_BEHAVIOR_EMPTY) + get_json_behavior(jexpr->on_error, context, "ERROR"); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_tablefunc - Parse back a table function + * ---------- + */ +static void +get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + /* XMLTABLE and JSON_TABLE are the only existing implementations. */ + + if (tf->functype == TFT_XMLTABLE) + get_xmltable(tf, context, showimplicit); + else if (tf->functype == TFT_JSON_TABLE) + get_json_table(tf, context, showimplicit); +} + /* ---------- * get_from_clause - Parse back a FROM clause * diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 584bf7001a..91453681e3 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1879,6 +1879,8 @@ typedef struct TableFuncScanState ExprState *rowexpr; /* state for row-generating expression */ List *colexprs; /* state for column-generating expression */ List *coldefexprs; /* state for column default expressions */ + List *colvalexprs; /* state for column value expression */ + List *passingvalexprs; /* state for PASSING argument expression */ List *ns_names; /* same as TableFunc.ns_names */ List *ns_uris; /* list of states of namespace URI exprs */ Bitmapset *notnulls; /* nullability flag for each output column */ diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 5e149b0266..d8466a2699 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -112,6 +112,8 @@ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *expr, JsonFormat *format); extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); +extern Node *makeJsonTableJoinedPlan(JsonTablePlanJoinType type, + Node *plan1, Node *plan2, int location); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 7d63a37d5f..b15f71cc92 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1733,6 +1733,19 @@ typedef enum JsonQuotes */ typedef char *JsonPathSpec; +/* + * JsonTableColumnType - + * enumeration of JSON_TABLE column types + */ +typedef enum JsonTableColumnType +{ + JTC_FOR_ORDINALITY, + JTC_REGULAR, + JTC_EXISTS, + JTC_FORMATTED, + JTC_NESTED, +} JsonTableColumnType; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1786,6 +1799,83 @@ typedef struct JsonFuncExpr int location; /* token location, or -1 if unknown */ } JsonFuncExpr; +/* + * JsonTableColumn - + * untransformed representation of JSON_TABLE column + */ +typedef struct JsonTableColumn +{ + NodeTag type; + JsonTableColumnType coltype; /* column type */ + char *name; /* column name */ + TypeName *typeName; /* column type name */ + char *pathspec; /* path specification, if any */ + char *pathname; /* path name, if any */ + JsonFormat *format; /* JSON format clause, if specified */ + JsonWrapper wrapper; /* WRAPPER behavior for formatted columns */ + bool omit_quotes; /* omit or keep quotes on scalar strings? */ + List *columns; /* nested columns */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + int location; /* token location, or -1 if unknown */ +} JsonTableColumn; + +/* + * JsonTablePlanType - + * flags for JSON_TABLE plan node types representation + */ +typedef enum JsonTablePlanType +{ + JSTP_DEFAULT, + JSTP_SIMPLE, + JSTP_JOINED, +} JsonTablePlanType; + +/* + * JsonTablePlanJoinType - + * flags for JSON_TABLE join types representation + */ +typedef enum JsonTablePlanJoinType +{ + JSTPJ_INNER = 0x01, + JSTPJ_OUTER = 0x02, + JSTPJ_CROSS = 0x04, + JSTPJ_UNION = 0x08, +} JsonTablePlanJoinType; + +typedef struct JsonTablePlan JsonTablePlan; + +/* + * JsonTablePlan - + * untransformed representation of JSON_TABLE plan node + */ +struct JsonTablePlan +{ + NodeTag type; + JsonTablePlanType plan_type; /* plan type */ + JsonTablePlanJoinType join_type; /* join type (for joined plan only) */ + JsonTablePlan *plan1; /* first joined plan */ + JsonTablePlan *plan2; /* second joined plan */ + char *pathname; /* path name (for simple plan only) */ + int location; /* token location, or -1 if unknown */ +}; + +/* + * JsonTable - + * untransformed representation of JSON_TABLE + */ +typedef struct JsonTable +{ + NodeTag type; + JsonCommon *common; /* common JSON path syntax fields */ + List *columns; /* list of JsonTableColumn */ + JsonTablePlan *plan; /* join plan, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + Alias *alias; /* table alias in FROM clause */ + bool lateral; /* does it have LATERAL prefix? */ + int location; /* token location, or -1 if unknown */ +} JsonTable; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 3937114743..cea15cc4e5 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -86,8 +86,14 @@ typedef struct RangeVar int location; } RangeVar; +typedef enum TableFuncType +{ + TFT_XMLTABLE, + TFT_JSON_TABLE +} TableFuncType; + /* - * TableFunc - node for a table function, such as XMLTABLE. + * TableFunc - node for a table function, such as XMLTABLE or JSON_TABLE. * * Entries in the ns_names list are either String nodes containing * literal namespace names, or NULL pointers to represent DEFAULT. @@ -95,6 +101,8 @@ typedef struct RangeVar typedef struct TableFunc { NodeTag type; + /* XMLTABLE or JSON_TABLE */ + TableFuncType functype; /* list of namespace URI expressions */ List *ns_uris pg_node_attr(query_jumble_ignore); /* list of namespace names or NULL */ @@ -115,8 +123,14 @@ typedef struct TableFunc List *colexprs; /* list of column default expressions */ List *coldefexprs pg_node_attr(query_jumble_ignore); + /* list of column value expressions */ + List *colvalexprs pg_node_attr(query_jumble_ignore); + /* list of PASSING argument expressions */ + List *passingvalexprs pg_node_attr(query_jumble_ignore); /* nullability flag for each output column */ Bitmapset *notnulls pg_node_attr(query_jumble_ignore); + /* JSON_TABLE plan */ + Node *plan pg_node_attr(query_jumble_ignore); /* counts from 0; -1 if none specified */ int ordinalitycol pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ @@ -1552,7 +1566,8 @@ typedef enum JsonExprOp { JSON_VALUE_OP, /* JSON_VALUE() */ JSON_QUERY_OP, /* JSON_QUERY() */ - JSON_EXISTS_OP /* JSON_EXISTS() */ + JSON_EXISTS_OP, /* JSON_EXISTS() */ + JSON_TABLE_OP /* JSON_TABLE() */ } JsonExprOp; /* @@ -1770,6 +1785,48 @@ typedef struct JsonExpr int location; /* token location, or -1 if unknown */ } JsonExpr; +/* + * JsonTablePath + * A JSON path expression to be computed as part of evaluating + * a JSON_TABLE plan node + */ +typedef struct JsonTablePath +{ + NodeTag type; + + Const *value; + char *name; +} JsonTablePath; + +/* + * JsonTableParent - + * transformed representation of parent JSON_TABLE plan node + */ +typedef struct JsonTableParent +{ + NodeTag type; + JsonTablePath *path; + Node *child; /* nested columns, if any */ + bool outerJoin; /* outer or inner join for nested columns? */ + int colMin; /* min column index in the resulting column + * list */ + int colMax; /* max column index in the resulting column + * list */ + bool errorOnError; /* ERROR/EMPTY ON ERROR behavior */ +} JsonTableParent; + +/* + * JsonTableSibling - + * transformed representation of joined sibling JSON_TABLE plan node + */ +typedef struct JsonTableSibling +{ + NodeTag type; + Node *larg; /* left join node */ + Node *rarg; /* right join node */ + bool cross; /* cross or union join? */ +} JsonTableSibling; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0954d9fc7b..f88a6c9ac6 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -242,6 +242,7 @@ PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_table", JSON_TABLE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) @@ -284,6 +285,7 @@ PG_KEYWORD("names", NAMES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("national", NATIONAL, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("natural", NATURAL, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("nchar", NCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("nested", NESTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("new", NEW, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("next", NEXT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("nfc", NFC, UNRESERVED_KEYWORD, BARE_LABEL) @@ -334,7 +336,9 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h index 8e26482db4..7152a8ef48 100644 --- a/src/include/parser/parse_clause.h +++ b/src/include/parser/parse_clause.h @@ -51,4 +51,7 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle, extern Index assignSortGroupRef(TargetEntry *tle, List *tlist); extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList); +/* functions in parse_jsontable.c */ +extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt); + #endif /* PARSE_CLAUSE_H */ diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index 4dae78a98c..b7ada77cb1 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -15,6 +15,7 @@ #define JSONPATH_H #include "fmgr.h" +#include "executor/tablefunc.h" #include "nodes/pg_list.h" #include "nodes/primnodes.h" #include "utils/jsonb.h" @@ -288,4 +289,6 @@ extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars); +extern PGDLLIMPORT const TableFuncRoutine JsonbTableRoutine; + #endif diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out index 8b87580752..995f267404 100644 --- a/src/test/regress/expected/json_sqljson.out +++ b/src/test/regress/expected/json_sqljson.out @@ -16,3 +16,9 @@ ERROR: JSON_QUERY() is not yet implemented for the json type LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); ^ HINT: Try casting the argument to jsonb +-- JSON_TABLE +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); +ERROR: JSON_TABLE() is not yet implemented for the json type +LINE 1: SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo ... + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out index 22f8679917..4323ed6b35 100644 --- a/src/test/regress/expected/jsonb_sqljson.out +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -1040,3 +1040,1072 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.dateti ERROR: functions in index expression must be marked IMMUTABLE CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; +-- JSON_TABLE +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); +ERROR: syntax error at or near "(" +LINE 1: SELECT JSON_TABLE('[]', '$'); + ^ +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); +ERROR: syntax error at or near ")" +LINE 1: SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + ^ +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); +ERROR: JSON_TABLE function has 1 columns available but 2 columns specified +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + foo +----- +(0 rows) + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + item | foo +------+----- + 123 | +(1 row) + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + js | id | id2 | int | text | char(4) | bool | numeric | domain | js | jb | jst | jsc | jsv | jsb | jsbq | aaa | aaa1 | exists1 | exists2 | exists3 | exists4 | js2 | jsb2w | jsb2q | ia | ta | jba +---------------------------------------------------------------------------------------+----+-----+-----+---------+---------+------+---------+---------+--------------+--------------+--------------+------+------+--------------+--------------+-----+------+---------+---------+---------+---------+--------------+----------------+--------------+----+----+----- + 1 | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [] | | | | | | | | | | | | | | | | | | | | | | | | | | | + {} | 1 | 1 | | | | | | | {} | {} | {} | {} | {} | {} | {} | | | f | 0 | | false | {} | [{}] | {} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 2 | 2 | 1 | 1.23 | 1.23 | | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | | | f | 0 | | false | 1.23 | [1.23] | 1.23 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 3 | 3 | 2 | 2 | 2 | | 2 | 2 | "2" | "2" | "2" | "2" | "2" | "2" | 2 | | | f | 0 | | false | "2" | ["2"] | 2 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 4 | 4 | | aaaaaaa | aaaa | | | aaaaaaa | "aaaaaaa" | "aaaaaaa" | "aaaaaaa" | "aaa | "aaa | "aaaaaaa" | | | | f | 0 | | false | "aaaaaaa" | ["aaaaaaa"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 5 | 5 | | foo | foo | | | | "foo" | "foo" | "foo" | "foo | "foo | "foo" | | | | f | 0 | | false | "foo" | ["foo"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 6 | 6 | | | | | | | null | null | null | null | null | null | null | | | f | 0 | | false | null | [null] | null | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 7 | 7 | 0 | false | fals | f | | false | false | false | false | fals | fals | false | false | | | f | 0 | | false | false | [false] | false | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 8 | 8 | 1 | true | true | t | | true | true | true | true | true | true | true | true | | | f | 0 | | false | true | [true] | true | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 9 | 9 | | | | | | | {"aaa": 123} | {"aaa": 123} | {"aaa": 123} | {"aa | {"aa | {"aaa": 123} | {"aaa": 123} | 123 | 123 | t | 1 | 1 | true | {"aaa": 123} | [{"aaa": 123}] | {"aaa": 123} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 10 | 10 | | [1,2] | [1,2 | | | [1,2] | "[1,2]" | "[1,2]" | "[1,2]" | "[1, | "[1, | "[1,2]" | [1, 2] | | | f | 0 | | false | "[1,2]" | ["[1,2]"] | [1, 2] | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 11 | 11 | | "str" | "str | | | "str" | "\"str\"" | "\"str\"" | "\"str\"" | "\"s | "\"s | "\"str\"" | "str" | | | f | 0 | | false | "\"str\"" | ["\"str\""] | "str" | | | +(14 rows) + +-- JSON_TABLE: Test backward parsing +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); +\sv jsonb_table_view +CREATE OR REPLACE VIEW public.jsonb_table_view AS + SELECT id, + id2, + "int", + text, + "char(4)", + bool, + "numeric", + domain, + js, + jb, + jst, + jsc, + jsv, + jsb, + jsbq, + aaa, + aaa1, + exists1, + exists2, + exists3, + js2, + jsb2w, + jsb2q, + ia, + ta, + jba, + a1, + b1, + a11, + a21, + a22 + FROM JSON_TABLE( + 'null'::jsonb, '$[*]' AS json_table_path_1 + PASSING + 1 + 2 AS a, + '"foo"'::json AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, + "int" integer PATH '$', + text text PATH '$', + "char(4)" character(4) PATH '$', + bool boolean PATH '$', + "numeric" numeric PATH '$', + domain jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc character(4) FORMAT JSON PATH '$', + jsv character varying(4) FORMAT JSON PATH '$', + jsb jsonb PATH '$', + jsbq jsonb PATH '$' OMIT QUOTES, + aaa integer PATH '$."aaa"', + aaa1 integer PATH '$."aaa"', + exists1 boolean EXISTS PATH '$."aaa"', + exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia integer[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 + COLUMNS ( + a1 integer PATH '$."a1"', + b1 text PATH '$."b1"', + NESTED PATH '$[*]' AS "p1 1" + COLUMNS ( + a11 text PATH '$."a11"' + ) + ), + NESTED PATH '$[2]' AS p2 + COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" + COLUMNS ( + a21 text PATH '$."a21"' + ), + NESTED PATH '$[*]' AS p22 + COLUMNS ( + a22 text PATH '$."a22"' + ) + ) + ) + PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22)))) + ) +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Table Function Scan on "json_table" + Output: "json_table".id, "json_table".id2, "json_table"."int", "json_table".text, "json_table"."char(4)", "json_table".bool, "json_table"."numeric", "json_table".domain, "json_table".js, "json_table".jb, "json_table".jst, "json_table".jsc, "json_table".jsv, "json_table".jsb, "json_table".jsbq, "json_table".aaa, "json_table".aaa1, "json_table".exists1, "json_table".exists2, "json_table".exists3, "json_table".js2, "json_table".jsb2w, "json_table".jsb2q, "json_table".ia, "json_table".ta, "json_table".jba, "json_table".a1, "json_table".b1, "json_table".a11, "json_table".a21, "json_table".a22 + Table Function Call: JSON_TABLE('null'::jsonb, '$[*]' AS json_table_path_1 PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (id FOR ORDINALITY, id2 FOR ORDINALITY, "int" integer PATH '$', text text PATH '$', "char(4)" character(4) PATH '$', bool boolean PATH '$', "numeric" numeric PATH '$', domain jsonb_test_domain PATH '$', js json PATH '$', jb jsonb PATH '$', jst text FORMAT JSON PATH '$', jsc character(4) FORMAT JSON PATH '$', jsv character varying(4) FORMAT JSON PATH '$', jsb jsonb PATH '$', jsbq jsonb PATH '$' OMIT QUOTES, aaa integer PATH '$."aaa"', aaa1 integer PATH '$."aaa"', exists1 boolean EXISTS PATH '$."aaa"', exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, js2 json PATH '$', jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, jsb2q jsonb PATH '$' OMIT QUOTES, ia integer[] PATH '$', ta text[] PATH '$', jba jsonb[] PATH '$', NESTED PATH '$[1]' AS p1 COLUMNS (a1 integer PATH '$."a1"', b1 text PATH '$."b1"', NESTED PATH '$[*]' AS "p1 1" COLUMNS (a11 text PATH '$."a11"')), NESTED PATH '$[2]' AS p2 COLUMNS ( NESTED PATH '$[*]' AS "p2:1" COLUMNS (a21 text PATH '$."a21"'), NESTED PATH '$[*]' AS p22 COLUMNS (a22 text PATH '$."a22"'))) PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22))))) +(3 rows) + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + js | a +-------+--- + 1 | 1 + "err" | +(2 rows) + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; + a +--- + +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: jsonpath member accessor can only be applied to an object +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: no SQL/JSON item +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 1 +(1 row) + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); + a +--- + 0 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to smallint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to bigint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to real +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 E... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); + a +----- + fal +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to json +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to jsonb +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EX... + ^ +-- JSON_TABLE: nested paths and plans +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb '[]', '$' -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 4: NESTED PATH '$' COLUMNS ( -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: b +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +-- JSON_TABLE: plan validation +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p1) + ^ +DETAIL: Path name mismatch: expected p0 but p1 is given. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 UNION p1 UNION p11) + ^ +DETAIL: Expected INNER or OUTER JSON_TABLE plan node. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 8: NESTED PATH '$' AS p2 COLUMNS ( + ^ +DETAIL: Plan node for nested path p2 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 5: NESTED PATH '$' AS p11 COLUMNS ( foo int ), + ^ +DETAIL: Plan node for nested path p11 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) + ^ +DETAIL: Plan node contains some extra or duplicate sibling nodes. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 6: NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ^ +DETAIL: Plan node for nested path p12 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 9: NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ^ +DETAIL: Plan node for nested path p21 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + bar | foo | baz +-----+-----+----- +(0 rows) + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb 'null', 'strict $[*]' -- without root path name + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- JSON_TABLE: plan execution +CREATE TEMP TABLE jsonb_table_test (js jsonb); +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + n | a | c | b +---+----+----+--- + 1 | 1 | | + 2 | 2 | 10 | + 2 | 2 | | + 2 | 2 | 20 | + 2 | 2 | | 1 + 2 | 2 | | 2 + 2 | 2 | | 3 + 3 | 3 | | 1 + 3 | 3 | | 2 + 4 | -1 | | 1 + 4 | -1 | | 2 +(11 rows) + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + n | a | b | b1 | c | c1 | b +---+---+--------------+-----+------+----+----- + 1 | 1 | [1, 10] | 1 | 1 | | 101 + 1 | 1 | [1, 10] | 1 | null | | 101 + 1 | 1 | [1, 10] | 1 | 2 | | 101 + 1 | 1 | [1, 10] | 10 | 1 | | 110 + 1 | 1 | [1, 10] | 10 | null | | 110 + 1 | 1 | [1, 10] | 10 | 2 | | 110 + 1 | 1 | [2] | 2 | 1 | | 102 + 1 | 1 | [2] | 2 | null | | 102 + 1 | 1 | [2] | 2 | 2 | | 102 + 1 | 1 | [3, 30, 300] | 3 | 1 | | 103 + 1 | 1 | [3, 30, 300] | 3 | null | | 103 + 1 | 1 | [3, 30, 300] | 3 | 2 | | 103 + 1 | 1 | [3, 30, 300] | 30 | 1 | | 130 + 1 | 1 | [3, 30, 300] | 30 | null | | 130 + 1 | 1 | [3, 30, 300] | 30 | 2 | | 130 + 1 | 1 | [3, 30, 300] | 300 | 1 | | 400 + 1 | 1 | [3, 30, 300] | 300 | null | | 400 + 1 | 1 | [3, 30, 300] | 300 | 2 | | 400 + 2 | 2 | | | | | + 3 | | | | | | +(20 rows) + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + x | y | y | z +---+---+--------------+--- + 2 | 1 | [1, 2, 3] | 1 + 2 | 1 | [1, 2, 3] | 2 + 2 | 1 | [1, 2, 3] | 3 + 3 | 1 | [1, 2, 3] | 1 + 3 | 1 | [1, 2, 3] | 2 + 3 | 1 | [1, 2, 3] | 3 + 3 | 1 | [2, 3, 4, 5] | 2 + 3 | 1 | [2, 3, 4, 5] | 3 + 3 | 1 | [2, 3, 4, 5] | 4 + 3 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [1, 2, 3] | 1 + 4 | 1 | [1, 2, 3] | 2 + 4 | 1 | [1, 2, 3] | 3 + 4 | 1 | [2, 3, 4, 5] | 2 + 4 | 1 | [2, 3, 4, 5] | 3 + 4 | 1 | [2, 3, 4, 5] | 4 + 4 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [3, 4, 5, 6] | 3 + 4 | 1 | [3, 4, 5, 6] | 4 + 4 | 1 | [3, 4, 5, 6] | 5 + 4 | 1 | [3, 4, 5, 6] | 6 + 2 | 2 | [1, 2, 3] | 2 + 2 | 2 | [1, 2, 3] | 3 + 3 | 2 | [1, 2, 3] | 2 + 3 | 2 | [1, 2, 3] | 3 + 3 | 2 | [2, 3, 4, 5] | 2 + 3 | 2 | [2, 3, 4, 5] | 3 + 3 | 2 | [2, 3, 4, 5] | 4 + 3 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [1, 2, 3] | 2 + 4 | 2 | [1, 2, 3] | 3 + 4 | 2 | [2, 3, 4, 5] | 2 + 4 | 2 | [2, 3, 4, 5] | 3 + 4 | 2 | [2, 3, 4, 5] | 4 + 4 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [3, 4, 5, 6] | 3 + 4 | 2 | [3, 4, 5, 6] | 4 + 4 | 2 | [3, 4, 5, 6] | 5 + 4 | 2 | [3, 4, 5, 6] | 6 + 2 | 3 | [1, 2, 3] | 3 + 3 | 3 | [1, 2, 3] | 3 + 3 | 3 | [2, 3, 4, 5] | 3 + 3 | 3 | [2, 3, 4, 5] | 4 + 3 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [1, 2, 3] | 3 + 4 | 3 | [2, 3, 4, 5] | 3 + 4 | 3 | [2, 3, 4, 5] | 4 + 4 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [3, 4, 5, 6] | 3 + 4 | 3 | [3, 4, 5, 6] | 4 + 4 | 3 | [3, 4, 5, 6] | 5 + 4 | 3 | [3, 4, 5, 6] | 6 +(52 rows) + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; +ERROR: could not find jsonpath variable "x" +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); + json_value +------------ + foo +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_query +------------ + 123 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); + json_query +------------ + [123] +(1 row) + +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +ERROR: syntax error at or near " " of jsonpath input +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); +ERROR: only string constants supported in JSON_TABLE path specification +LINE 1: SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || '... + ^ diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql index 4f30fa46b9..df4a430d88 100644 --- a/src/test/regress/sql/json_sqljson.sql +++ b/src/test/regress/sql/json_sqljson.sql @@ -9,3 +9,7 @@ SELECT JSON_VALUE(NULL FORMAT JSON, '$'); -- JSON_QUERY SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + +-- JSON_TABLE + +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql index b8a6148b7b..fbd86f2084 100644 --- a/src/test/regress/sql/jsonb_sqljson.sql +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -325,3 +325,632 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; + +-- JSON_TABLE + +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); + +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); + +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); + +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + +-- JSON_TABLE: Test backward parsing + +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); + +\sv jsonb_table_view + +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; + +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); + +-- JSON_TABLE: nested paths and plans + +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; + +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; + +-- JSON_TABLE: plan validation + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; + +-- JSON_TABLE: plan execution + +CREATE TEMP TABLE jsonb_table_test (js jsonb); + +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); + +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; + +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 6e4c026492..642a29d8d5 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1303,6 +1303,7 @@ JsonPathKeyword JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback +JsonPathSpec JsonPathString JsonPathVarCallback JsonPathVariable @@ -1311,6 +1312,17 @@ JsonQuotes JsonReturning JsonSemAction JsonSerializeExpr +JsonTable +JsonTableColumn +JsonTableColumnType +JsonTableContext +JsonTableJoinState +JsonTableParent +JsonTablePlan +JsonTablePlanJoinType +JsonTablePlanType +JsonTableScanState +JsonTableSibling JsonTokenType JsonTransformStringValuesAction JsonTypeCategory @@ -2765,6 +2777,7 @@ TableFunc TableFuncRoutine TableFuncScan TableFuncScanState +TableFuncType TableInfo TableLikeClause TableSampleClause -- 2.35.3
v8-0003-SQL-JSON-query-functions.patch (application/octet-stream, 209.0K) download From af1e9a6e2204a336c2fcef172128c508ca43fa24 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Tue, 18 Jul 2023 17:58:35 +0900 Subject: [PATCH v8 3/5] SQL/JSON query functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This introduces the SQL/JSON functions for querying JSON data using jsonpath expressions. The functions are: JSON_EXISTS() JSON_QUERY() JSON_VALUE() All of these functions only operate on jsonb. The workaround for now is to cast the argument to jsonb. JSON_EXISTS() tests if the jsonpath expression applied to the jsonb value yields any values. JSON_VALUE() must return a single value, and an error occurs if it tries to return multiple values. JSON_QUERY() must return a json object or array, and there are various WRAPPER options for handling scalar or multi-value results. Both these functions have options for handling EMPTY and ERROR conditions. Author: Nikita Glukhov <n.gluhov@postgrespro.ru> Author: Teodor Sigaev <teodor@sigaev.ru> Author: Oleg Bartunov <obartunov@gmail.com> Author: Alexander Korotkov <aekorotkov@gmail.com> Author: Andrew Dunstan <andrew@dunslane.net> Author: Amit Langote <amitlangote09@gmail.com> Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- doc/src/sgml/func.sgml | 147 +++ src/backend/executor/execExpr.c | 432 ++++++++ src/backend/executor/execExprInterp.c | 502 ++++++++- src/backend/jit/llvm/llvmjit_expr.c | 250 +++++ src/backend/jit/llvm/llvmjit_types.c | 5 + src/backend/nodes/makefuncs.c | 15 + src/backend/nodes/nodeFuncs.c | 183 ++++ src/backend/optimizer/path/costsize.c | 3 +- src/backend/optimizer/util/clauses.c | 19 + src/backend/parser/gram.y | 348 ++++++- src/backend/parser/parse_collate.c | 7 + src/backend/parser/parse_expr.c | 519 ++++++++- src/backend/parser/parse_target.c | 15 + src/backend/utils/adt/formatting.c | 44 + src/backend/utils/adt/jsonb.c | 62 ++ src/backend/utils/adt/jsonfuncs.c | 170 ++- src/backend/utils/adt/jsonpath.c | 255 +++++ src/backend/utils/adt/jsonpath_exec.c | 391 ++++++- src/backend/utils/adt/ruleutils.c | 136 +++ src/include/executor/execExpr.h | 172 +++ src/include/executor/executor.h | 1 + src/include/nodes/execnodes.h | 3 + src/include/nodes/makefuncs.h | 1 + src/include/nodes/parsenodes.h | 48 + src/include/nodes/primnodes.h | 109 ++ src/include/parser/kwlist.h | 11 + src/include/utils/formatting.h | 2 +- src/include/utils/jsonb.h | 3 + src/include/utils/jsonfuncs.h | 5 + src/include/utils/jsonpath.h | 27 + src/interfaces/ecpg/preproc/ecpg.trailer | 28 + src/test/regress/expected/json_sqljson.out | 18 + src/test/regress/expected/jsonb_sqljson.out | 1042 +++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/json_sqljson.sql | 11 + src/test/regress/sql/jsonb_sqljson.sql | 327 ++++++ src/tools/pgindent/typedefs.list | 14 + 37 files changed, 5234 insertions(+), 93 deletions(-) create mode 100644 src/test/regress/expected/json_sqljson.out create mode 100644 src/test/regress/expected/jsonb_sqljson.out create mode 100644 src/test/regress/sql/json_sqljson.sql create mode 100644 src/test/regress/sql/jsonb_sqljson.sql diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index ac81f6b827..5dcd95f42f 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16989,6 +16989,153 @@ array w/o UK? | t </tbody> </tgroup> </table> + + <para> + <xref linkend="functions-sqljson-querying"/> details the SQL/JSON + functions that can be used to query JSON data. + </para> + + <note> + <para> + SQL/JSON paths can only be applied to the <type>jsonb</type> type, so it + might be necessary to cast the <replaceable>context_item</replaceable> + argument of these functions to <type>jsonb</type>. + </para> + </note> + + <table id="functions-sqljson-querying"> + <title>SQL/JSON Query Functions</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="func_table_entry"><para role="func_signature"> + Function signature + </para> + <para> + Description + </para> + <para> + Example(s) + </para></entry> + </row> + </thead> + <tbody> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_exists</primary></indexterm> + <function>json_exists</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>TRUE</literal> | <literal>FALSE</literal> |<literal> UNKNOWN</literal> | <literal>ERROR</literal> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns true if the SQL/JSON <replaceable>path_expression</replaceable> + applied to the <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s yields any items. + The <literal>ON ERROR</literal> clause specifies what is returned if + an error occurs. Note that if the <replaceable>path_expression</replaceable> + is <literal>strict</literal>, an error is generated if it yields no items. + The default value is <literal>UNKNOWN</literal> which causes a NULL + result. + </para> + <para> + <literal>json_exists(jsonb '{"key1": [1,2,3]}', 'strict $.key1[*] ? (@ > 2)')</literal> + <returnvalue>t</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'lax $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>f</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'strict $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>ERROR: jsonpath array subscript is out of bounds</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_value</primary></indexterm> + <function>json_value</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> + <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. The extracted value must be + a single <acronym>SQL/JSON</acronym> scalar item. For results that + are objects or arrays, use the <function>json_query</function> + function instead. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + The <literal>ON EMPTY</literal> clause specifies the behavior if the + <replaceable>path_expression</replaceable> yields no value at all. + The <literal>ON ERROR</literal> clause specifies the behavior if an + error occurs as a result of <type>jsonpath</type> evaluation + (including cast to the output type) or execution of + <literal>ON EMPTY</literal> behavior (that was caused by empty result + of <type>jsonpath</type> evaluation). + </para> + <para> + <literal>json_value(jsonb '"123.45"', '$' RETURNING float)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_value(jsonb '"03:04 2015-02-01"', '$.datetime("HH24:MI&nbsp;YYYY-MM-DD")' RETURNING date)</literal> + <returnvalue>2015-02-01</returnvalue> + </para> + <para> + <literal>json_value(jsonb '[1,2]', 'strict $[*]' DEFAULT 9 ON ERROR)</literal> + <returnvalue>9</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_query</primary></indexterm> + <function>json_query</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional> + <optional> { <literal>WITHOUT</literal> | <literal>WITH</literal> { <literal>CONDITIONAL</literal> | <optional><literal>UNCONDITIONAL</literal></optional> } } <optional> <literal>ARRAY</literal> </optional> <literal>WRAPPER</literal> </optional> + <optional> { <literal>KEEP</literal> | <literal>OMIT</literal> } <literal>QUOTES</literal> <optional> <literal>ON SCALAR STRING</literal> </optional> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. + This function must return a JSON string, so if the path expression + returns multiple SQL/JSON items, you must wrap the result using the + <literal>WITH WRAPPER</literal> clause. If the wrapper is + <literal>UNCONDITIONAL</literal>, an array wrapper will always + be applied, even if the returned value is already a single JSON object + or array, but if it is <literal>CONDITIONAL</literal>, it will not be + applied to a single array or object. <literal>UNCONDITIONAL</literal> + is the default. + If the result is a scalar string, by default the value returned will have + surrounding quotes making it a valid JSON value. However, this behavior + is reversed if <literal>OMIT QUOTES</literal> is specified. + The <literal>ON ERROR</literal> and <literal>ON EMPTY</literal> + clauses have similar semantics to those clauses for + <function>json_value</function>. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + </para> + <para> + <literal>json_query(jsonb '[1,[2,3],null]', 'lax $[*][1]' WITH CONDITIONAL WRAPPER)</literal> + <returnvalue>[3]</returnvalue> + </para></entry> + </row> + </tbody> + </tgroup> + </table> + </sect2> <sect2 id="functions-sqljson-path"> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 6ca4098bef..d3d2ce00d1 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -49,6 +49,7 @@ #include "utils/builtins.h" #include "utils/datum.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -88,6 +89,16 @@ static void ExecBuildAggTransCall(ExprState *state, AggState *aggstate, FunctionCallInfo fcinfo, AggStatePerTrans pertrans, int transno, int setno, int setoff, bool ishash, bool nullcheck); +static void ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch); +static Datum GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null); +static JsonCoercionState *ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull); +static JsonItemCoercionsState *ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull); /* @@ -2411,6 +2422,14 @@ ExecInitExprRec(Expr *node, ExprState *state, break; } + case T_JsonExpr: + { + JsonExpr *jexpr = castNode(JsonExpr, node); + + ExecInitJsonExpr(jexpr, state, resv, resnull, &scratch); + break; + } + case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -4178,3 +4197,416 @@ ExecBuildParamSetEqual(TupleDesc desc, return state; } + +/* + * Push steps to evaluate a JsonExpr and its various subsidiary expressions. + */ +static void +ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch) +{ + JsonExprState *jsestate = palloc0(sizeof(JsonExprState)); + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + ListCell *argexprlc; + ListCell *argnamelc; + int skip_step_off = -1; + int passing_args_step_off = -1; + int coercion_step_off = -1; + int coercion_finish_step_off = -1; + int behavior_step_off = -1; + int onempty_expr_step_off = -1; + int onempty_jump_step_off = -1; + int onerror_expr_step_off = -1; + int onerror_jump_step_off = -1; + List *adjust_jumps = NIL; + ListCell *lc; + ExprEvalStep *as; + + jsestate->jsexpr = jexpr; + + /* + * Add steps to compute formatted_expr, pathspec, and PASSING arg + * expressions as things that must be evaluated *before* the actual JSON + * path expression. + */ + ExecInitExprRec((Expr *) jexpr->formatted_expr, state, + &pre_eval->formatted_expr.value, + &pre_eval->formatted_expr.isnull); + ExecInitExprRec((Expr *) jexpr->path_spec, state, + &pre_eval->pathspec.value, + &pre_eval->pathspec.isnull); + + /* + * Before pushing steps for PASSING args, push a step to decide whether to + * skip evaluating the args and the JSON path expression depending on + * whether either of formatted_expr and pathspec is NULL; see + * ExecEvalJsonExprSkip(). + */ + scratch->opcode = EEOP_JSONEXPR_SKIP; + scratch->d.jsonexpr_skip.jsestate = jsestate; + skip_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* PASSING args. */ + jsestate->pre_eval.args = NIL; + passing_args_step_off = state->steps_len; + forboth(argexprlc, jexpr->passing_values, + argnamelc, jexpr->passing_names) + { + Expr *argexpr = (Expr *) lfirst(argexprlc); + String *argname = lfirst_node(String, argnamelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(argname->sval); + var->typid = exprType((Node *) argexpr); + var->typmod = exprTypmod((Node *) argexpr); + + ExecInitExprRec((Expr *) argexpr, state, &var->value, &var->isnull); + + pre_eval->args = lappend(pre_eval->args, var); + } + + /* + * Step for the actual JSON path evaluation; see ExecEvalJson() and + * ExecEvalJsonExpr(). + */ + scratch->opcode = EEOP_JSONEXPR_PATH; + scratch->d.jsonexpr.jsestate = jsestate; + ExprEvalPushStep(state, scratch); + + /* + * Push steps to control the evaluation of expressions based on the result + * of JSON path evaluation. + */ + + /* + * Step to handle ON ERROR and ON EMPTY behavior; see + * ExecEvalJsonExprBehavior(). + */ + scratch->opcode = EEOP_JSONEXPR_BEHAVIOR; + scratch->d.jsonexpr_behavior.jsestate = jsestate; + behavior_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Step(s) to evaluate ON EMPTY expression */ + onempty_expr_step_off = state->steps_len; + if (jexpr->on_empty && + jexpr->on_empty->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_empty->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_empty->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_empty, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onempty_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + } + } + + /* Step(s) to evaluate ON ERROR expression */ + onerror_expr_step_off = state->steps_len; + if (jexpr->on_error && + jexpr->on_error->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_error->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_error->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_error, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onerror_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set later */ + ExprEvalPushStep(state, scratch); + } + } + + /* + * Step to handle applying coercion to the JSON item returned by + * EEOP_JSONEXPR_PATH or to the ON EMPTY/ERROR expression as + * EEOP_JSONEXPR_BEHAVIOR decides; see ExecEvalJsonExprCoercion(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Initialize coercion expression(s). */ + if (jexpr->result_coercion) + { + jsestate->result_jcstate = + ExecInitJsonCoercion(scratch, state, jexpr->result_coercion, + resv, resnull); + /* See the comment above. */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* computed later */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + if (jexpr->coercions) + { + /* + * ExecPrepareJsonItemCoercion() called by ExecEvalJsonExpr() chooses + * one for a given JSON item returned by JsonPathValue(). + */ + jsestate->item_jcstates = + ExecInitJsonItemCoercions(scratch, state, jexpr->coercions, + resv, resnull); + } + + /* + * And a step to clean up after the coercion step; see + * ExecEvalJsonExprCoercionFinish(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION_FINISH; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_finish_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* + * Adjust jump target addresses in various post-eval steps now that we + * have all the steps in place. + */ + + /* EEOP_JSONEXPR_SKIP */ + Assert(skip_step_off >= 0); + as = &state->steps[skip_step_off]; + as->d.jsonexpr_skip.jump_coercion = coercion_step_off; + as->d.jsonexpr_skip.jump_passing_args = passing_args_step_off; + + /* EEOP_JSONEXPR_BEHAVIOR */ + Assert(behavior_step_off >= 0); + as = &state->steps[behavior_step_off]; + as->d.jsonexpr_behavior.jump_onerror_expr = onerror_expr_step_off; + as->d.jsonexpr_behavior.jump_onempty_expr = onempty_expr_step_off; + as->d.jsonexpr_behavior.jump_coercion = coercion_step_off; + as->d.jsonexpr_behavior.jump_skip_coercion = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION */ + Assert(coercion_step_off >= 0); + as = &state->steps[coercion_step_off]; + as->d.jsonexpr_coercion.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION_FINISH */ + Assert(coercion_finish_step_off >= 0); + as = &state->steps[coercion_finish_step_off]; + as->d.jsonexpr_coercion_finish.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion_finish.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JUMP steps */ + + /* + * Ones after ON EMPTY and ON ERROR non-default expressions should jump to + * the coercion step. + */ + if (onempty_jump_step_off >= 0) + { + as = &state->steps[onempty_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + if (onerror_jump_step_off >= 0) + { + as = &state->steps[onerror_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + + /* The rest should jump to the end. */ + foreach(lc, adjust_jumps) + { + as = &state->steps[lfirst_int(lc)]; + as->d.jump.jumpdone = state->steps_len; + } + + /* + * Set RETURNING type's input function used by ExecEvalJsonExprCoercion(). + */ + if (jexpr->omit_quotes || + (jexpr->result_coercion && jexpr->result_coercion->via_io)) + { + Oid typinput; + FmgrInfo *finfo; + + /* lookup the result type's input function */ + getTypeInputInfo(jexpr->returning->typid, &typinput, + &jsestate->input.typioparam); + finfo = palloc0(sizeof(FmgrInfo)); + fmgr_info(typinput, finfo); + jsestate->input.finfo = finfo; + } +} + +/* + * Evaluate a JSON error/empty behavior result. + */ +static Datum +GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null) +{ + *is_null = false; + + switch (behavior->btype) + { + case JSON_BEHAVIOR_EMPTY_ARRAY: + return JsonbPGetDatum(JsonbMakeEmptyArray()); + + case JSON_BEHAVIOR_EMPTY_OBJECT: + return JsonbPGetDatum(JsonbMakeEmptyObject()); + + case JSON_BEHAVIOR_TRUE: + return BoolGetDatum(true); + + case JSON_BEHAVIOR_FALSE: + return BoolGetDatum(false); + + case JSON_BEHAVIOR_NULL: + case JSON_BEHAVIOR_UNKNOWN: + case JSON_BEHAVIOR_EMPTY: + *is_null = true; + return (Datum) 0; + + case JSON_BEHAVIOR_DEFAULT: + /* Always handled in the caller. */ + Assert(false); + return (Datum) 0; + + default: + elog(ERROR, "unrecognized SQL/JSON behavior %d", behavior->btype); + return (Datum) 0; + } +} + +/* Initialize one JsonCoercion for execution. */ +static JsonCoercionState * +ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull) +{ + JsonCoercionState *jcstate = palloc0(sizeof(JsonCoercionState)); + + jcstate->coercion = coercion; + if (coercion && coercion->expr) + { + Datum *save_innermost_caseval; + bool *save_innermost_casenull; + + jcstate->jump_eval_expr = state->steps_len; + + /* Push step(s) to compute cstate->coercion. */ + save_innermost_caseval = state->innermost_caseval; + save_innermost_casenull = state->innermost_casenull; + + state->innermost_caseval = resv; + state->innermost_casenull = resnull; + + ExecInitExprRec((Expr *) coercion->expr, state, resv, resnull); + + state->innermost_caseval = save_innermost_caseval; + state->innermost_casenull = save_innermost_casenull; + } + else + jcstate->jump_eval_expr = -1; + + return jcstate; +} + +/* + * Push steps to evaluate coercions from a given JsonItemCoercions, which + * contains all possible coercions that may need to be applied to JSON + * items coming from evaluating the parent JsonExpr. + */ +static JsonItemCoercionsState * +ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull) +{ + JsonCoercion **coercion; + JsonItemCoercionsState *item_jcstates = palloc0(sizeof(JsonItemCoercionsState)); + JsonCoercionState **item_jcstate; + ExprEvalStep *as; + List *adjust_jumps = NIL; + ListCell *lc; + + /* Push the steps of individual coercions. */ + for (coercion = &coercions->null, + item_jcstate = &item_jcstates->null; + coercion <= &coercions->composite; + coercion++, item_jcstate++) + { + *item_jcstate = ExecInitJsonCoercion(scratch, state, *coercion, + resv, resnull); + + /* Emit JUMP step to skip past other coercions' steps. */ + scratch->opcode = EEOP_JUMP; + + /* + * Remember JUMP step address to set the actual jump target address + * below. + */ + adjust_jumps = lappend_int(adjust_jumps, state->steps_len); + ExprEvalPushStep(state, scratch); + } + + foreach(lc, adjust_jumps) + { + int jump_step_id = lfirst_int(lc); + + as = &state->steps[jump_step_id]; + as->d.jump.jumpdone = state->steps_len; + } + + return item_jcstates; +} diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 76e59691e5..4c97e714ea 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -74,6 +74,7 @@ #include "utils/json.h" #include "utils/jsonb.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/timestamp.h" @@ -152,6 +153,9 @@ static TupleDesc get_cached_rowtype(Oid type_id, int32 typmod, bool *changed); static void ExecEvalRowNullInt(ExprState *state, ExprEvalStep *op, ExprContext *econtext, bool checkisnull); +static Datum ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate); /* fast-path evaluation functions */ static Datum ExecJustInnerVar(ExprState *state, ExprContext *econtext, bool *isnull); @@ -480,6 +484,11 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_XMLEXPR, &&CASE_EEOP_JSON_CONSTRUCTOR, &&CASE_EEOP_IS_JSON, + &&CASE_EEOP_JSONEXPR_SKIP, + &&CASE_EEOP_JSONEXPR_PATH, + &&CASE_EEOP_JSONEXPR_BEHAVIOR, + &&CASE_EEOP_JSONEXPR_COERCION, + &&CASE_EEOP_JSONEXPR_COERCION_FINISH, &&CASE_EEOP_AGGREF, &&CASE_EEOP_GROUPING_FUNC, &&CASE_EEOP_WINDOW_FUNC, @@ -1186,8 +1195,14 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) /* second and third arguments are already set up */ fcinfo_in->isnull = false; + + /* Pass down soft-error capture node if any. */ + fcinfo_in->context = state->escontext; + d = FunctionCallInvoke(fcinfo_in); *op->resvalue = d; + if (SOFT_ERROR_OCCURRED(state->escontext)) + *op->resnull = true; /* Should get null result if and only if str is NULL */ if (str == NULL) @@ -1195,7 +1210,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) Assert(*op->resnull); Assert(fcinfo_in->isnull); } - else + else if (!SOFT_ERROR_OCCURRED(state->escontext)) { Assert(!*op->resnull); Assert(!fcinfo_in->isnull); @@ -1543,6 +1558,38 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_JSONEXPR_PATH) + { + /* too complex for an inline implementation */ + ExecEvalJsonExpr(state, op, econtext); + EEO_NEXT(); + } + + EEO_CASE(EEOP_JSONEXPR_SKIP) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprSkip(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_BEHAVIOR) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprBehavior(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercion(state, op, econtext, + *op->resvalue, *op->resnull)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION_FINISH) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercionFinish(state, op)); + } + EEO_CASE(EEOP_AGGREF) { /* @@ -3745,7 +3792,7 @@ ExecEvalConstraintCheck(ExprState *state, ExprEvalStep *op) { if (!*op->d.domaincheck.checknull && !DatumGetBool(*op->d.domaincheck.checkvalue)) - ereport(ERROR, + errsave(state->escontext, (errcode(ERRCODE_CHECK_VIOLATION), errmsg("value for domain %s violates check constraint \"%s\"", format_type_be(op->d.domaincheck.resulttype), @@ -4138,6 +4185,457 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op) *op->resvalue = BoolGetDatum(res); } +/* + * Evaluate given JsonExpr by performing the specified JSON operation. + * + * This also populates the JsonExprPostEvalState with the information needed + * by the subsequent steps that handle the specified JsonBehavior. + */ +void +ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) +{ + JsonExprState *jsestate = op->d.jsonexpr.jsestate; + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonExpr *jexpr = jsestate->jsexpr; + Datum item; + Datum res = (Datum) 0; + bool resnull = true; + JsonPath *path; + bool *error; + bool *empty; + + item = pre_eval->formatted_expr.value; + path = DatumGetJsonPathP(pre_eval->pathspec.value); + + /* Reset JsonExprPostEvalState for this evaluation. */ + memset(post_eval, 0, sizeof(*post_eval)); + + /* Respect ON ERROR behavior during path evaluation. */ + jsestate->throw_error = (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR); + + /* + * Be sure to save the error (if needed) and empty statuses for the + * EEOP_JSONEXPR_BEHAVIOR step to peruse. + */ + error = !jsestate->throw_error ? &post_eval->error : NULL; + empty = &post_eval->empty; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + res = JsonPathQuery(item, path, jexpr->wrapper, empty, error, + pre_eval->args); + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + resnull = !DatumGetPointer(res); + break; + + case JSON_VALUE_OP: + { + JsonbValue *jbv = JsonPathValue(item, path, empty, error, + pre_eval->args); + + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + if (!jbv) /* NULL or empty */ + { + resnull = true; + break; + } + + Assert(!*empty); + + resnull = false; + + /* coerce scalar item to the output type */ + if (jexpr->returning->typid == JSONOID || + jexpr->returning->typid == JSONBOID) + { + /* Use result coercion from json[b] to the output type */ + res = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + break; + } + + /* + * Error out if no cast exists to coerce SQL/JSON item to the + * the output type. + */ + Assert(post_eval->item_jcstate == NULL); + res = ExecPrepareJsonItemCoercion(jbv, + jsestate->item_jcstates, + &post_eval->item_jcstate); + if (post_eval->item_jcstate && + post_eval->item_jcstate->coercion && + (post_eval->item_jcstate->coercion->via_io || + post_eval->item_jcstate->coercion->via_populate)) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * Coercion via I/O means here that the cast to the target + * type simply does not exist. + */ + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE), + errmsg("SQL/JSON item cannot be cast to target type"))); + } + break; + } + + case JSON_EXISTS_OP: + { + bool exists = JsonPathExists(item, path, + pre_eval->args, + error); + + resnull = error && *error; + res = BoolGetDatum(exists); + break; + } + + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * If the ON EMPTY behavior is to cause an error, do so here. Other + * behaviors will be handled by the caller. + */ + if (*empty) + { + Assert(jexpr->on_empty); /* it is not JSON_EXISTS */ + + if (jexpr->on_empty->btype == JSON_BEHAVIOR_ERROR) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + ereport(ERROR, + (errcode(ERRCODE_NO_SQL_JSON_ITEM), + errmsg("no SQL/JSON item"))); + } + } + + *op->resvalue = res; + *op->resnull = resnull; +} + +/* + * Skip calling ExecEvalJson() on the given JsonExpr? + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_skip.jsestate; + + /* + * Skip if either of the input expressions has turned out to be NULL, + * though do execute domain checks for NULLs, which are handled by the + * coercion step. + */ + if (jsestate->pre_eval.formatted_expr.isnull || + jsestate->pre_eval.pathspec.isnull) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_skip.jump_coercion; + } + + /* + * Go evaluate the PASSING args if any and subsequently JSON path itself. + */ + return op->d.jsonexpr_skip.jump_passing_args; +} + +/* + * Returns the step address to perform the JsonBehavior applicable to + * the JSON item that resulted from evaluating the given JsonExpr. + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_behavior.jsestate; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonBehavior *behavior = NULL; + int jump_to = -1; + + if (post_eval->error || post_eval->coercion_error) + { + behavior = jsestate->jsexpr->on_error; + jump_to = op->d.jsonexpr_behavior.jump_onerror_expr; + } + else if (post_eval->empty) + { + behavior = jsestate->jsexpr->on_empty; + jump_to = op->d.jsonexpr_behavior.jump_onempty_expr; + } + else if (!post_eval->coercion_done) + { + /* + * If no error or the JSON item is not empty, directly go to the + * coercion step to coerce the item as is. + */ + return op->d.jsonexpr_behavior.jump_coercion; + } + + Assert(behavior); + + /* + * Set up for coercion step that will run to coerce a non-default behavior + * value. It should use result_coercion, if any. Errors that may occur + * should be thrown for JSON ops other than JSON_VALUE_OP. + */ + if (behavior->btype != JSON_BEHAVIOR_DEFAULT) + { + post_eval->item_jcstate = NULL; + jsestate->throw_error = (jsestate->jsexpr->op != JSON_VALUE_OP); + } + + Assert(jump_to >= 0); + return jump_to; +} + +/* + * Evaluate or return the step address to evaluate a coercion of a JSON item + * to the target type. The former if the coercion must be done right away by + * calling its input function or by calling json_populate_type(). + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull) +{ + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonExpr *jexpr = jsestate->jsexpr; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonCoercionState *item_jcstate = post_eval->item_jcstate; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + + if (item_jcstate == NULL && + jsestate->jsexpr->op != JSON_EXISTS_OP) + { + ErrorSaveContext escontext = {T_ErrorSaveContext}; + Node *escontext_p; + JsonCoercion *coercion; + Jsonb *jb; + + escontext_p = !jsestate->throw_error ? (Node *) &escontext : NULL; + coercion = result_jcstate ? result_jcstate->coercion : NULL; + jb = resnull ? NULL : DatumGetJsonbP(res); + + if ((coercion && coercion->via_io) || + (jexpr->omit_quotes && !resnull && + JB_ROOT_IS_SCALAR(jb))) + { + /* strip quotes and call typinput function */ + char *str = resnull ? NULL : JsonbUnquote(jb); + bool type_is_domain; + + type_is_domain = (getBaseType(jexpr->returning->typid) != + jexpr->returning->typid); + + /* + * Catch errors only if the type is not a domain, because errors + * caused by a domain's constraint failure must be thrown right + * away. + */ + if (!InputFunctionCallSafe(jsestate->input.finfo, str, + jsestate->input.typioparam, + jexpr->returning->typmod, + !type_is_domain ? escontext_p : NULL, + op->resvalue)) + { + post_eval->error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + else if (coercion && coercion->via_populate) + { + *op->resvalue = json_populate_type(res, JSONBOID, + jexpr->returning->typid, + jexpr->returning->typmod, + &post_eval->cache, + econtext->ecxt_per_query_memory, + op->resnull, + escontext_p); + if (SOFT_ERROR_OCCURRED(escontext_p)) + { + post_eval->error = true; + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + } + + if (!jsestate->throw_error) + { + post_eval->escontext.type = T_ErrorSaveContext; + state->escontext = (Node *) &post_eval->escontext; + } + + if (item_jcstate && item_jcstate->jump_eval_expr >= 0) + return item_jcstate->jump_eval_expr; + else if (result_jcstate && result_jcstate->jump_eval_expr >= 0) + return result_jcstate->jump_eval_expr; + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; +} + +/* + * Checks if the coercion evaluation led to an error. If an error did occur, + * this returns the address of the step that handles the error, otherwise + * the step after the coercion step, which finishes the JsonExpr processing. + */ +int +ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op) +{ + JsonExprPostEvalState *post_eval = + &op->d.jsonexpr_coercion_finish.jsestate->post_eval; + + if (SOFT_ERROR_OCCURRED(state->escontext)) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + post_eval->coercion_error = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_error; + } + + /* Reset. */ + state->escontext = NULL; + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_done; +} + +/* + * Prepare SQL/JSON item coercion to the output type. Returned a datum of the + * corresponding SQL type and a pointer to the coercion state. + */ +static Datum +ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate) +{ + JsonCoercionState *item_jcstate; + Datum res; + JsonbValue buf; + + if (item->type == jbvBinary && + JsonContainerIsScalar(item->val.binary.data)) + { + bool res PG_USED_FOR_ASSERTS_ONLY; + + res = JsonbExtractScalar(item->val.binary.data, &buf); + item = &buf; + Assert(res); + } + + /* get coercion state reference and datum of the corresponding SQL type */ + switch (item->type) + { + case jbvNull: + item_jcstate = item_jcstates->null; + res = (Datum) 0; + break; + + case jbvString: + item_jcstate = item_jcstates->string; + res = PointerGetDatum(cstring_to_text_with_len(item->val.string.val, + item->val.string.len)); + break; + + case jbvNumeric: + item_jcstate = item_jcstates->numeric; + res = NumericGetDatum(item->val.numeric); + break; + + case jbvBool: + item_jcstate = item_jcstates->boolean; + res = BoolGetDatum(item->val.boolean); + break; + + case jbvDatetime: + res = item->val.datetime.value; + switch (item->val.datetime.typid) + { + case DATEOID: + item_jcstate = item_jcstates->date; + break; + case TIMEOID: + item_jcstate = item_jcstates->time; + break; + case TIMETZOID: + item_jcstate = item_jcstates->timetz; + break; + case TIMESTAMPOID: + item_jcstate = item_jcstates->timestamp; + break; + case TIMESTAMPTZOID: + item_jcstate = item_jcstates->timestamptz; + break; + default: + elog(ERROR, "unexpected jsonb datetime type oid %u", + item->val.datetime.typid); + return (Datum) 0; + } + break; + + case jbvArray: + case jbvObject: + case jbvBinary: + item_jcstate = item_jcstates->composite; + res = JsonbPGetDatum(JsonbValueToJsonb(item)); + break; + + default: + elog(ERROR, "unexpected jsonb value type %d", item->type); + return (Datum) 0; + } + + *p_item_jcstate = item_jcstate; + + return res; +} + /* * ExecEvalGroupingFunc diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 00d7b8110b..da3870cba6 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -1860,6 +1860,256 @@ llvm_compile_expr(ExprState *state) LLVMBuildBr(b, opblocks[opno + 1]); break; + case EEOP_JSONEXPR_PATH: + build_EvalXFunc(b, mod, "ExecEvalJsonExpr", + v_state, op, v_econtext); + LLVMBuildBr(b, opblocks[opno + 1]); + break; + + case EEOP_JSONEXPR_SKIP: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprSkip() to decide if JSON path + * evaluation can be skipped. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprSkip"), + params, lengthof(params), ""); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_skip.jump_coercion, which signifies + * skipping of JSON path evaluation, else to the next step + * which must point to the steps to evaluate PASSING args, + * if any, or to the JSON path evaluation. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_skip.jump_coercion), + ""), + opblocks[op->d.jsonexpr_skip.jump_coercion], + opblocks[opno + 1]); + break; + } + + case EEOP_JSONEXPR_BEHAVIOR: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + LLVMBasicBlockRef b_jump_onerror_default; + + /* + * Call ExecEvalJsonExprBehavior() to decide if ON EMPTY + * or ON ERROR behavior must be invoked depending on what + * JSON path evaluation returned. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprBehavior"), + params, lengthof(params), ""); + + b_jump_onerror_default = + l_bb_before_v(opblocks[opno + 1], + "op.%d.jsonexpr_behavior_jump_onerror_default", opno); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_behavior.jump_coercion, else to the + * next block, one that checks whether to evaluate the ON + * ERROR default expression. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_coercion), + ""), + opblocks[op->d.jsonexpr_behavior.jump_coercion], + b_jump_onerror_default); + + /* + * Block that checks whether to evaluate the ON ERROR + * default expression. + * + * Jump to evaluate the ON ERROR default expression if the + * returned address is the same as + * jsonexpr_behavior.jump_onerror_default, else jump to + * evaluate the ON EMPTY default expression. + */ + LLVMPositionBuilderAtEnd(b, b_jump_onerror_default); + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_onerror_expr), + ""), + opblocks[op->d.jsonexpr_behavior.jump_onerror_expr], + opblocks[op->d.jsonexpr_behavior.jump_onempty_expr]); + break; + } + case EEOP_JSONEXPR_COERCION: + { + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonItemCoercionsState *item_jcstates = jsestate->item_jcstates; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + LLVMValueRef v_ret; + LLVMValueRef params[5]; + + /* + * Call ExecEvalJsonExprCoercion() to evaluate appropriate + * coercion. This will return the step address to jump + * to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + params[2] = v_econtext; + params[3] = v_resvaluep; + params[4] = v_resnullp; + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercion"), + params, lengthof(params), ""); + + /* + * Jump to handle a coercion error if the returned address + * is the same as jsonexpr_coercion.jump_coercion_error, + * else to the step after coercion (coercion done!). + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + + if (item_jcstates) + { + JsonCoercionState **item_jcstate; + int n_coercions = (int) + (item_jcstates->composite - item_jcstates->null) + 1; + int i; + LLVMBasicBlockRef *b_coercions; + + + /* + * Will create a block for each coercion below to + * check whether to evaluate the coercion's expression + * if there's one or to skip to the end if not. + */ + b_coercions = palloc((n_coercions + 1) * sizeof(LLVMBasicBlockRef)); + for (i = 0; i < n_coercions + 1; i++) + b_coercions[i] = + l_bb_before_v(opblocks[opno + 1], + "op.%d.json_item_coercion.%d", + opno, i); + + /* Jump to check first coercion */ + LLVMBuildBr(b, b_coercions[0]); + + /* + * Add conditional branches for individual coercion's + * expressions + */ + for (item_jcstate = &item_jcstates->null, i = 0; + item_jcstate <= &item_jcstates->composite; + item_jcstate++, i++) + { + /* Block for this coercion */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + + /* + * Jump to evaluate the coercion's expression if + * the address returned is the same as this + * coercion's jump_eval_expr (that is, if it is + * valid), else check the next coercion's. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const((*item_jcstate)->jump_eval_expr), + ""), + (*item_jcstate)->jump_eval_expr >= 0 ? + opblocks[(*item_jcstate)->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + b_coercions[i + 1]); + } + + /* + * A placeholder block that the last coercion's block + * might jump to, which unconditionally jumps to end + * of coercions. + */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + LLVMBuildBr(b, opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + + /* + * Jump to evaluate the result_coercion's expression if + * none of the above coercions matched, that is, if + * there's one. + */ + if (result_jcstate) + { + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(result_jcstate->jump_eval_expr), + ""), + result_jcstate->jump_eval_expr >= 0 ? + opblocks[result_jcstate->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + break; + } + + case EEOP_JSONEXPR_COERCION_FINISH: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprCoercionFinish() to check whether + * an coercion error occurred, in which case we must jump + * to whatever step handles the error. This returns the + * step address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercionFinish"), + params, lengthof(params), ""); + + /* + * Jump to the step that handles coercion error if the + * returned address is the same as + * jsonexpr_coercion_finish.jump_coercion_error, else to + * jsonexpr_coercion_finish.jump_coercion_done. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion_finish.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_done]); + break; + } + case EEOP_AGGREF: { LLVMValueRef v_aggno; diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c index 41ac4c6f45..cf3ced3427 100644 --- a/src/backend/jit/llvm/llvmjit_types.c +++ b/src/backend/jit/llvm/llvmjit_types.c @@ -135,6 +135,11 @@ void *referenced_functions[] = ExecEvalXmlExpr, ExecEvalJsonConstructor, ExecEvalJsonIsPredicate, + ExecEvalJsonExprSkip, + ExecEvalJsonExprBehavior, + ExecEvalJsonExprCoercion, + ExecEvalJsonExprCoercionFinish, + ExecEvalJsonExpr, MakeExpandedObjectReadOnlyInternal, slot_getmissingattrs, slot_getsomeattrs_int, diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index c6c310d253..7bcc12b04f 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -863,6 +863,21 @@ makeJsonValueExpr(Expr *expr, JsonFormat *format) return jve; } +/* + * makeJsonBehavior - + * creates a JsonBehavior node + */ +JsonBehavior * +makeJsonBehavior(JsonBehaviorType type, Node *default_expr) +{ + JsonBehavior *behavior = makeNode(JsonBehavior); + + behavior->btype = type; + behavior->default_expr = default_expr; + + return behavior; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index dda964bd19..d31371bb4d 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -236,6 +236,12 @@ exprType(const Node *expr) case T_JsonIsPredicate: type = BOOLOID; break; + case T_JsonExpr: + type = ((const JsonExpr *) expr)->returning->typid; + break; + case T_JsonCoercion: + type = exprType(((const JsonCoercion *) expr)->expr); + break; case T_NullTest: type = BOOLOID; break; @@ -495,6 +501,10 @@ exprTypmod(const Node *expr) return exprTypmod((Node *) ((const JsonValueExpr *) expr)->formatted_expr); case T_JsonConstructorExpr: return ((const JsonConstructorExpr *) expr)->returning->typmod; + case T_JsonExpr: + return ((JsonExpr *) expr)->returning->typmod; + case T_JsonCoercion: + return exprTypmod(((const JsonCoercion *) expr)->expr); case T_CoerceToDomain: return ((const CoerceToDomain *) expr)->resulttypmod; case T_CoerceToDomainValue: @@ -971,6 +981,22 @@ exprCollation(const Node *expr) /* IS JSON's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + coll = InvalidOid; + else if (coercion->expr) + coll = exprCollation(coercion->expr); + else if (coercion->via_io || coercion->via_populate) + coll = coercion->collation; + else + coll = InvalidOid; + } + break; + case T_NullTest: /* NullTest's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ @@ -1207,6 +1233,21 @@ exprSetCollation(Node *expr, Oid collation) case T_JsonIsPredicate: Assert(!OidIsValid(collation)); /* result is always boolean */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + Assert(!OidIsValid(collation)); + else if (coercion->expr) + exprSetCollation(coercion->expr, collation); + else if (coercion->via_io || coercion->via_populate) + coercion->collation = collation; + else + Assert(!OidIsValid(collation)); + } + break; case T_NullTest: /* NullTest's result is boolean ... */ Assert(!OidIsValid(collation)); /* ... so never set a collation */ @@ -1510,6 +1551,15 @@ exprLocation(const Node *expr) case T_JsonIsPredicate: loc = ((const JsonIsPredicate *) expr)->location; break; + case T_JsonExpr: + { + const JsonExpr *jsexpr = (const JsonExpr *) expr; + + /* consider both function name and leftmost arg */ + loc = leftmostLoc(jsexpr->location, + exprLocation(jsexpr->formatted_expr)); + } + break; case T_NullTest: { const NullTest *nexpr = (const NullTest *) expr; @@ -2262,6 +2312,54 @@ expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + if (WALK(jexpr->formatted_expr)) + return true; + if (WALK(jexpr->result_coercion)) + return true; + if (WALK(jexpr->passing_values)) + return true; + /* we assume walker doesn't care about passing_names */ + if (jexpr->on_empty && + WALK(jexpr->on_empty->default_expr)) + return true; + if (WALK(jexpr->on_error->default_expr)) + return true; + if (WALK(jexpr->coercions)) + return true; + } + break; + case T_JsonCoercion: + return WALK(((JsonCoercion *) node)->expr); + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + + if (WALK(coercions->null)) + return true; + if (WALK(coercions->string)) + return true; + if (WALK(coercions->numeric)) + return true; + if (WALK(coercions->boolean)) + return true; + if (WALK(coercions->date)) + return true; + if (WALK(coercions->time)) + return true; + if (WALK(coercions->timetz)) + return true; + if (WALK(coercions->timestamp)) + return true; + if (WALK(coercions->timestamptz)) + return true; + if (WALK(coercions->composite)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: @@ -3261,6 +3359,54 @@ expression_tree_mutator_impl(Node *node, return (Node *) newnode; } + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + JsonExpr *newnode; + + FLATCOPY(newnode, jexpr, JsonExpr); + MUTATE(newnode->path_spec, jexpr->path_spec, Node *); + MUTATE(newnode->formatted_expr, jexpr->formatted_expr, Node *); + MUTATE(newnode->result_coercion, jexpr->result_coercion, JsonCoercion *); + MUTATE(newnode->passing_values, jexpr->passing_values, List *); + /* assume mutator does not care about passing_names */ + if (newnode->on_empty) + MUTATE(newnode->on_empty->default_expr, + jexpr->on_empty->default_expr, Node *); + MUTATE(newnode->on_error->default_expr, + jexpr->on_error->default_expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonCoercion: + { + JsonCoercion *coercion = (JsonCoercion *) node; + JsonCoercion *newnode; + + FLATCOPY(newnode, coercion, JsonCoercion); + MUTATE(newnode->expr, coercion->expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + JsonItemCoercions *newnode; + + FLATCOPY(newnode, coercions, JsonItemCoercions); + MUTATE(newnode->null, coercions->null, JsonCoercion *); + MUTATE(newnode->string, coercions->string, JsonCoercion *); + MUTATE(newnode->numeric, coercions->numeric, JsonCoercion *); + MUTATE(newnode->boolean, coercions->boolean, JsonCoercion *); + MUTATE(newnode->date, coercions->date, JsonCoercion *); + MUTATE(newnode->time, coercions->time, JsonCoercion *); + MUTATE(newnode->timetz, coercions->timetz, JsonCoercion *); + MUTATE(newnode->timestamp, coercions->timestamp, JsonCoercion *); + MUTATE(newnode->timestamptz, coercions->timestamptz, JsonCoercion *); + MUTATE(newnode->composite, coercions->composite, JsonCoercion *); + return (Node *) newnode; + } + break; case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -3947,6 +4093,43 @@ raw_expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonArgument: + return WALK(((JsonArgument *) node)->val); + case T_JsonCommon: + { + JsonCommon *jc = (JsonCommon *) node; + + if (WALK(jc->expr)) + return true; + if (WALK(jc->pathspec)) + return true; + if (WALK(jc->passing)) + return true; + } + break; + case T_JsonBehavior: + { + JsonBehavior *jb = (JsonBehavior *) node; + + if (jb->btype == JSON_BEHAVIOR_DEFAULT && + WALK(jb->default_expr)) + return true; + } + break; + case T_JsonFuncExpr: + { + JsonFuncExpr *jfe = (JsonFuncExpr *) node; + + if (WALK(jfe->common)) + return true; + if (jfe->output && WALK(jfe->output)) + return true; + if (WALK(jfe->on_empty)) + return true; + if (WALK(jfe->on_error)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index ef475d95a1..f58c275b4b 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -4609,7 +4609,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context) IsA(node, SQLValueFunction) || IsA(node, XmlExpr) || IsA(node, CoerceToDomain) || - IsA(node, NextValueExpr)) + IsA(node, NextValueExpr) || + IsA(node, JsonExpr)) { /* Treat all these as having cost 1 */ context->total.per_tuple += cpu_operator_cost; diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index da258968b8..e40cfab4b7 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -53,6 +53,7 @@ #include "utils/fmgroids.h" #include "utils/json.h" #include "utils/jsonb.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/syscache.h" @@ -412,6 +413,24 @@ contain_mutable_functions_walker(Node *node, void *context) /* Check all subnodes */ } + if (IsA(node, JsonExpr)) + { + JsonExpr *jexpr = castNode(JsonExpr, node); + Const *cnst; + + if (!IsA(jexpr->path_spec, Const)) + return true; + + cnst = castNode(Const, jexpr->path_spec); + + Assert(cnst->consttype == JSONPATHOID); + if (cnst->constisnull) + return false; + + return jspIsMutable(DatumGetJsonPathP(cnst->constvalue), + jexpr->passing_names, jexpr->passing_values); + } + if (IsA(node, SQLValueFunction)) { /* all variants of SQLValueFunction are stable */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 26cbebb707..0de553f5f9 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -278,6 +278,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MergeWhenClause *mergewhen; struct KeyActions *keyactions; struct KeyAction *keyaction; + JsonBehavior *jsbehavior; + JsonQuotes js_quotes; } %type <node> stmt toplevel_stmt schema_stmt routine_body_stmt @@ -650,14 +652,22 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_returning_clause_opt json_name_and_value json_aggregate_func + json_api_common_syntax + json_argument %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt + json_arguments %type <ival> json_encoding_clause_opt json_predicate_type_constraint + json_wrapper_behavior %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt +%type <jsbehavior> json_value_behavior + json_query_behavior + json_exists_behavior +%type <js_quotes> json_quotes_clause_opt /* * Non-keyword token types. These are hard-wired into the "flex" lexer. @@ -694,7 +704,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT - COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT + COMMITTED COMPRESSION CONCURRENTLY CONDITIONAL CONFIGURATION CONFLICT CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY COST CREATE CROSS CSV CUBE CURRENT_P CURRENT_CATALOG CURRENT_DATE CURRENT_ROLE CURRENT_SCHEMA @@ -705,8 +715,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P DOUBLE_P DROP - EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT - EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION + EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE + EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION EXTENSION EXTERNAL EXTRACT FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR @@ -721,10 +731,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION - JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG - JSON_SCALAR JSON_SERIALIZE + JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE - KEY KEYS + KEY KEYS KEEP LABEL LANGUAGE LARGE_P LAST_P LATERAL_P LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL @@ -738,7 +748,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC - OBJECT_P OF OFF OFFSET OIDS OLD ON ONLY OPERATOR OPTION OPTIONS OR + OBJECT_P OF OFF OFFSET OIDS OLD OMIT ON ONLY OPERATOR OPTION OPTIONS OR ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER @@ -747,7 +757,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION - QUOTE + QUOTE QUOTES RANGE READ REAL REASSIGN RECHECK RECURSIVE REF_P REFERENCES REFERENCING REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA @@ -758,7 +768,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); SEQUENCE SEQUENCES SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P - START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P + START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRING_P STRIP_P SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN @@ -766,7 +776,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); TREAT TRIGGER TRIM TRUE_P TRUNCATE TRUSTED TYPE_P TYPES_P - UESCAPE UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN + UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING @@ -15662,6 +15672,192 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_error = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->on_error = $10; + n->location = @1; + $$ = (Node *) n; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->location = @1; + $$ = (Node *) p; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + json_exists_behavior ON ERROR_P + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->on_error = $5; + p->location = @1; + $$ = (Node *) p; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->on_error = $8; + n->location = @1; + $$ = (Node *) n; + } ; @@ -16388,6 +16584,72 @@ opt_asymmetric: ASYMMETRIC ; /* SQL/JSON support */ +json_api_common_syntax: + json_value_expr ',' a_expr /* i.e. a json_path */ + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + + | json_value_expr ',' a_expr /* i.e. a json_path */ + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = $5; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = $7; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_arguments: + json_argument { $$ = list_make1($1); } + | json_arguments ',' json_argument { $$ = lappend($1, $3); } + ; + +json_argument: + json_value_expr AS ColLabel + { + JsonArgument *n = makeNode(JsonArgument); + + n->val = (JsonValueExpr *) $1; + n->name = $3; + $$ = (Node *) n; + } + ; + json_value_expr: a_expr json_format_clause_opt { @@ -16411,6 +16673,50 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; +json_query_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + | EMPTY_P ARRAY { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + | EMPTY_P OBJECT_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + /* non-standard, for Oracle compatibility only */ + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + ; + +json_exists_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | TRUE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_TRUE, NULL); } + | FALSE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_FALSE, NULL); } + | UNKNOWN { $$ = makeJsonBehavior(JSON_BEHAVIOR_UNKNOWN, NULL); } + ; + +json_value_behavior: + NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + ; + +/* ARRAY is a noise word */ +json_wrapper_behavior: + WITHOUT WRAPPER { $$ = JSW_NONE; } + | WITHOUT ARRAY WRAPPER { $$ = JSW_NONE; } + | WITH WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL ARRAY WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL WRAPPER { $$ = JSW_UNCONDITIONAL; } + | /* empty */ { $$ = JSW_NONE; } + ; + +json_quotes_clause_opt: + KEEP QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_KEEP; } + | KEEP QUOTES { $$ = JS_QUOTES_KEEP; } + | OMIT QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_OMIT; } + | OMIT QUOTES { $$ = JS_QUOTES_OMIT; } + | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } + ; + json_returning_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17013,6 +17319,7 @@ unreserved_keyword: | COMMIT | COMMITTED | COMPRESSION + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17049,10 +17356,12 @@ unreserved_keyword: | DOUBLE_P | DROP | EACH + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17102,6 +17411,7 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION + | KEEP | KEY | KEYS | LABEL @@ -17148,6 +17458,7 @@ unreserved_keyword: | OFF | OIDS | OLD + | OMIT | OPERATOR | OPTION | OPTIONS @@ -17178,6 +17489,7 @@ unreserved_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REASSIGN @@ -17237,6 +17549,7 @@ unreserved_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUPPORT @@ -17259,6 +17572,7 @@ unreserved_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNKNOWN | UNLISTEN @@ -17319,10 +17633,13 @@ col_name_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE | LEAST | NATIONAL | NCHAR @@ -17555,6 +17872,7 @@ bare_label_keyword: | COMMITTED | COMPRESSION | CONCURRENTLY + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17607,11 +17925,13 @@ bare_label_keyword: | DROP | EACH | ELSE + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | END_P | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17681,10 +18001,14 @@ bare_label_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE + | KEEP | KEY | KEYS | LABEL @@ -17745,6 +18069,7 @@ bare_label_keyword: | OFF | OIDS | OLD + | OMIT | ONLY | OPERATOR | OPTION @@ -17782,6 +18107,7 @@ bare_label_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REAL @@ -17850,6 +18176,7 @@ bare_label_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUBSTRING @@ -17884,6 +18211,7 @@ bare_label_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNIQUE | UNKNOWN diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c index 9f6afc351c..692e5d1225 100644 --- a/src/backend/parser/parse_collate.c +++ b/src/backend/parser/parse_collate.c @@ -691,6 +691,13 @@ assign_collations_walker(Node *node, assign_collations_context *context) &loccontext); } break; + case T_JsonExpr: + + /* + * Context item and PASSING arguments are already + * marked with collations in parse_expr.c. + */ + break; default: /* diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index a2644dbc77..89343e0d6a 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -90,6 +90,7 @@ static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); static Node *transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr); +static Node *transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *p); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -353,6 +354,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); break; + case T_JsonFuncExpr: + result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3225,7 +3230,7 @@ makeJsonByteaToTextConversion(Node *expr, JsonFormat *format, int location) static Node * transformJsonValueExpr(ParseState *pstate, const char *constructName, JsonValueExpr *ve, JsonFormatType default_format, - Oid targettype) + Oid targettype, bool isarg) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3262,6 +3267,35 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, else format = ve->format->format_type; } + else if (isarg) + { + /* Pass SQL/JSON item types directly without conversion to json[b]. */ + switch (exprtype) + { + case TEXTOID: + case NUMERICOID: + case BOOLOID: + case INT2OID: + case INT4OID: + case INT8OID: + case FLOAT4OID: + case FLOAT8OID: + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + return expr; + + default: + if (typcategory == TYPCATEGORY_STRING) + return expr; + /* else convert argument to json[b] type */ + break; + } + + format = default_format; + } else if (exprtype == JSONOID || exprtype == JSONBOID) format = JS_FORMAT_DEFAULT; /* do not format json[b] types */ else @@ -3273,7 +3307,8 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, Node *coerced; bool cast_is_needed = OidIsValid(targettype); - if (!cast_is_needed && + if (!isarg && + !cast_is_needed && exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), @@ -3614,7 +3649,7 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) Node *val = transformJsonValueExpr(pstate, "JSON_OBJECT()", kv->value, JS_FORMAT_DEFAULT, - InvalidOid); + InvalidOid, false); args = lappend(args, key); args = lappend(args, val); @@ -3800,7 +3835,7 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) key = transformExprRecurse(pstate, (Node *) agg->arg->key); val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", agg->arg->value, - JS_FORMAT_DEFAULT, InvalidOid); + JS_FORMAT_DEFAULT, InvalidOid, false); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3856,9 +3891,8 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) Oid aggfnoid; Oid aggtype; - arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", - agg->arg, - JS_FORMAT_DEFAULT, InvalidOid); + arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", agg->arg, + JS_FORMAT_DEFAULT, InvalidOid, false); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3905,9 +3939,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) { JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, "JSON_ARRAY()", - jsval, - JS_FORMAT_DEFAULT, - InvalidOid); + jsval, JS_FORMAT_DEFAULT, + InvalidOid, false); args = lappend(args, val); } @@ -4065,7 +4098,7 @@ transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) * function-like CASTs. */ arg = transformJsonValueExpr(pstate, "JSON()", jsexpr->expr, - JS_FORMAT_JSON, returning->typid); + JS_FORMAT_JSON, returning->typid, false); } return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, @@ -4099,9 +4132,8 @@ static Node * transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) { Node *arg = transformJsonValueExpr(pstate, "JSON_SERIALIZE()", - expr->expr, - JS_FORMAT_JSON, - InvalidOid); + expr->expr, JS_FORMAT_JSON, + InvalidOid, false); JsonReturning *returning; if (expr->output) @@ -4136,3 +4168,462 @@ transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), NULL, returning, false, false, expr->location); } + +/* + * Transform a JSON PASSING clause. + */ +static void +transformJsonPassingArgs(ParseState *pstate, char *constructName, + JsonFormatType format, List *args, + List **passing_values, List **passing_names) +{ + ListCell *lc; + + *passing_values = NIL; + *passing_names = NIL; + + foreach(lc, args) + { + JsonArgument *arg = castNode(JsonArgument, lfirst(lc)); + Node *expr = transformJsonValueExpr(pstate, constructName, + arg->val, format, + InvalidOid, true); + + assign_expr_collations(pstate, expr); + + *passing_values = lappend(*passing_values, expr); + *passing_names = lappend(*passing_names, makeString(arg->name)); + } +} + +/* + * Transform a JSON BEHAVIOR clause. + */ +static JsonBehavior * +transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior, + JsonBehaviorType default_behavior) +{ + JsonBehaviorType behavior_type = default_behavior; + Node *default_expr = NULL; + + if (behavior) + { + behavior_type = behavior->btype; + if (behavior_type == JSON_BEHAVIOR_DEFAULT) + default_expr = transformExprRecurse(pstate, behavior->default_expr); + } + return makeJsonBehavior(behavior_type, default_expr); +} + +/* + * Common code for JSON_VALUE, JSON_QUERY, JSON_EXISTS transformation + * into a JsonExpr node. + */ +static JsonExpr * +transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = makeNode(JsonExpr); + Node *pathspec; + JsonFormatType format; + char *constructName; + + if (func->common->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("JSON_TABLE path name is not allowed here"), + parser_errposition(pstate, func->location))); + + jsexpr->location = func->location; + jsexpr->op = func->op; + switch (jsexpr->op) + { + case JSON_VALUE_OP: + constructName = "JSON_VALUE()"; + break; + case JSON_QUERY_OP: + constructName = "JSON_QUERY()"; + break; + case JSON_EXISTS_OP: + constructName = "JSON_EXISTS()"; + break; + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); + break; + } + jsexpr->formatted_expr = transformJsonValueExpr(pstate, constructName, + func->common->expr, + JS_FORMAT_DEFAULT, + InvalidOid, false); + + assign_expr_collations(pstate, jsexpr->formatted_expr); + + /* format is determined by context item type */ + format = exprType(jsexpr->formatted_expr) == JSONBOID ? JS_FORMAT_JSONB : JS_FORMAT_JSON; + + jsexpr->result_coercion = NULL; + jsexpr->omit_quotes = false; + + jsexpr->format = func->common->expr->format; + + pathspec = transformExprRecurse(pstate, func->common->pathspec); + + jsexpr->path_spec = + coerce_to_target_type(pstate, pathspec, exprType(pathspec), + JSONPATHOID, -1, + COERCION_EXPLICIT, COERCE_IMPLICIT_CAST, + exprLocation(pathspec)); + if (!jsexpr->path_spec) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("JSON path expression must be type %s, not type %s", + "jsonpath", format_type_be(exprType(pathspec))), + parser_errposition(pstate, exprLocation(pathspec)))); + + /* transform and coerce to json[b] passing arguments */ + transformJsonPassingArgs(pstate, constructName, format, + func->common->passing, + &jsexpr->passing_values, + &jsexpr->passing_names); + + if (func->op != JSON_EXISTS_OP) + jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, + JSON_BEHAVIOR_NULL); + + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + func->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : + JSON_BEHAVIOR_NULL); + + return jsexpr; +} + +/* + * Assign default JSON returning type from the specified format or from + * the context item type. + */ +static void +assignDefaultJsonReturningType(Node *context_item, JsonFormat *context_format, + JsonReturning *ret) +{ + bool is_jsonb; + + ret->format = copyObject(context_format); + + if (ret->format->format_type == JS_FORMAT_DEFAULT) + is_jsonb = exprType(context_item) == JSONBOID; + else + is_jsonb = ret->format->format_type == JS_FORMAT_JSONB; + + ret->typid = is_jsonb ? JSONBOID : JSONOID; + ret->typmod = -1; +} + +/* + * Try to coerce expression to the output type or + * use json_populate_type() for composite, array and domain types or + * use coercion via I/O. + */ +static JsonCoercion * +coerceJsonExpr(ParseState *pstate, Node *expr, const JsonReturning *returning) +{ + char typtype; + JsonCoercion *coercion = makeNode(JsonCoercion); + + coercion->expr = coerceJsonFuncExpr(pstate, expr, returning, false); + + if (coercion->expr) + { + if (coercion->expr == expr) + coercion->expr = NULL; + + return coercion; + } + + typtype = get_typtype(returning->typid); + + if (returning->typid == RECORDOID || + typtype == TYPTYPE_COMPOSITE || + typtype == TYPTYPE_DOMAIN || + type_is_array(returning->typid)) + coercion->via_populate = true; + else + coercion->via_io = true; + + return coercion; +} + +/* + * Transform a JSON output clause of JSON_VALUE and JSON_QUERY. + */ +static void +transformJsonFuncExprOutput(ParseState *pstate, JsonFuncExpr *func, + JsonExpr *jsexpr) +{ + Node *expr = jsexpr->formatted_expr; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + /* JSON_VALUE returns text by default */ + if (func->op == JSON_VALUE_OP && !OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = TEXTOID; + jsexpr->returning->typmod = -1; + } + + if (OidIsValid(jsexpr->returning->typid)) + { + JsonReturning ret; + + if (func->op == JSON_VALUE_OP && + jsexpr->returning->typid != JSONOID && + jsexpr->returning->typid != JSONBOID) + { + /* Forced coercion via I/O for JSON_VALUE for non-JSON types */ + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = NULL; + jsexpr->result_coercion->via_io = true; + return; + } + + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, &ret); + + if (ret.typid != jsexpr->returning->typid || + ret.typmod != jsexpr->returning->typmod) + { + CaseTestExpr *cte = makeNode(CaseTestExpr); + + cte->typeId = exprType(expr); + cte->typeMod = exprTypmod(expr); + cte->collation = exprCollation(expr); + + Assert(cte->typeId == ret.typid); + Assert(cte->typeMod == ret.typmod); + + jsexpr->result_coercion = coerceJsonExpr(pstate, (Node *) cte, + jsexpr->returning); + } + } + else + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, + jsexpr->returning); +} + +/* + * Coerce an expression in JSON DEFAULT behavior to the target output type. + */ +static Node * +coerceDefaultJsonExpr(ParseState *pstate, JsonExpr *jsexpr, Node *defexpr) +{ + int location; + Oid exprtype; + + if (!defexpr) + return NULL; + + exprtype = exprType(defexpr); + location = exprLocation(defexpr); + + if (location < 0) + location = jsexpr->location; + + defexpr = coerce_to_target_type(pstate, + defexpr, + exprtype, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!defexpr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast DEFAULT expression type %s to %s", + format_type_be(exprtype), + format_type_be(jsexpr->returning->typid)), + parser_errposition(pstate, location))); + + return defexpr; +} + +/* + * Initialize SQL/JSON item coercion from the SQL type "typid" to the target + * "returning" type. + */ +static JsonCoercion * +initJsonItemCoercion(ParseState *pstate, Oid typid, + const JsonReturning *returning) +{ + Node *expr; + + if (typid == UNKNOWNOID) + { + expr = (Node *) makeNullConst(UNKNOWNOID, -1, InvalidOid); + } + else + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + + placeholder->typeId = typid; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + expr = (Node *) placeholder; + } + + return coerceJsonExpr(pstate, expr, returning); +} + +static void +initJsonItemCoercions(ParseState *pstate, JsonItemCoercions *coercions, + const JsonReturning *returning, Oid contextItemTypeId) +{ + struct + { + JsonCoercion **coercion; + Oid typid; + } *p, + coercionTypids[] = + { + {&coercions->null, UNKNOWNOID}, + {&coercions->string, TEXTOID}, + {&coercions->numeric, NUMERICOID}, + {&coercions->boolean, BOOLOID}, + {&coercions->date, DATEOID}, + {&coercions->time, TIMEOID}, + {&coercions->timetz, TIMETZOID}, + {&coercions->timestamp, TIMESTAMPOID}, + {&coercions->timestamptz, TIMESTAMPTZOID}, + {&coercions->composite, contextItemTypeId}, + {NULL, InvalidOid} + }; + + for (p = coercionTypids; p->coercion; p++) + *p->coercion = initJsonItemCoercion(pstate, p->typid, returning); +} + +/* + * Transform JSON_VALUE, JSON_QUERY, JSON_EXISTS functions into a JsonExpr node. + */ +static Node * +transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = transformJsonExprCommon(pstate, func); + const char *func_name = NULL; + Node *contextItemExpr = jsexpr->formatted_expr; + + /* + * Disallow FORMAT specification in the RETURNING clause of JSON_EXISTS() + * and JSON_VALUE(). + */ + if (func->output && + (func->op == JSON_VALUE_OP || func->op == JSON_EXISTS_OP)) + { + JsonFormat *format = func->output->returning->format; + + if(format->format_type != JS_FORMAT_DEFAULT || + format->encoding != JS_ENC_DEFAULT) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot specify FORMAT in RETURNING clause of %s", + func->op == JSON_VALUE_OP ? "JSON_VALUE()" : + "JSON_EXISTS()"), + parser_errposition(pstate, format->location))); + } + + switch (func->op) + { + case JSON_VALUE_OP: + func_name = "JSON_VALUE"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->coercions = makeNode(JsonItemCoercions); + initJsonItemCoercions(pstate, jsexpr->coercions, jsexpr->returning, + exprType(contextItemExpr)); + + break; + + case JSON_QUERY_OP: + func_name = "JSON_QUERY"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->wrapper = func->wrapper; + jsexpr->omit_quotes = func->omit_quotes; + + break; + + case JSON_EXISTS_OP: + func_name = "JSON_EXISTS"; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + if (!OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = BOOLOID; + jsexpr->returning->typmod = -1; + } + else if (jsexpr->returning->typid != BOOLOID) + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + int location = exprLocation((Node *) jsexpr); + + placeholder->typeId = BOOLOID; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = + coerce_to_target_type(pstate, (Node *) placeholder, BOOLOID, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!jsexpr->result_coercion->expr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(BOOLOID), + format_type_be(jsexpr->returning->typid)), + parser_coercion_errposition(pstate, location, (Node *) jsexpr))); + + if (jsexpr->result_coercion->expr == (Node *) placeholder) + jsexpr->result_coercion->expr = NULL; + } + break; + } + + if (exprType(contextItemExpr) != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("%s() is not yet implemented for the json type", func_name), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + return (Node *) jsexpr; +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 520d4f2a23..4f94fc69d6 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1979,6 +1979,21 @@ FigureColnameInternal(Node *node, char **name) /* make JSON_ARRAYAGG act like a regular function */ *name = "json_arrayagg"; return 2; + case T_JsonFuncExpr: + /* make SQL/JSON functions act like a regular function */ + switch (((JsonFuncExpr *) node)->op) + { + case JSON_QUERY_OP: + *name = "json_query"; + return 2; + case JSON_VALUE_OP: + *name = "json_value"; + return 2; + case JSON_EXISTS_OP: + *name = "json_exists"; + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index e27ea8ef97..188b5594e0 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -4426,6 +4426,50 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, } } +/* + * Parses the datetime format string in 'fmt_str' and returns true if it + * contains a timezone specifier, false if not. + */ +bool +datetime_format_has_tz(const char *fmt_str) +{ + bool incache; + int fmt_len = strlen(fmt_str); + int result; + FormatNode *format; + + if (fmt_len > DCH_CACHE_SIZE) + { + /* + * Allocate new memory if format picture is bigger than static cache + * and do not use cache (call parser always) + */ + incache = false; + + format = (FormatNode *) palloc((fmt_len + 1) * sizeof(FormatNode)); + + parse_format(format, fmt_str, DCH_keywords, + DCH_suff, DCH_index, DCH_FLAG, NULL); + } + else + { + /* + * Use cache buffers + */ + DCHCacheEntry *ent = DCH_cache_fetch(fmt_str, false); + + incache = true; + format = ent->format; + } + + result = DCH_datetime_type(format); + + if (!incache) + pfree(format); + + return result & DCH_ZONED; +} + /* * do_to_timestamp: shared code for to_timestamp and to_date * diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index 06ba409e64..d2b4da8ec8 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -2156,3 +2156,65 @@ jsonb_float8(PG_FUNCTION_ARGS) PG_RETURN_DATUM(retValue); } + +/* + * Construct an empty array jsonb. + */ +Jsonb * +JsonbMakeEmptyArray(void) +{ + JsonbValue jbv; + + jbv.type = jbvArray; + jbv.val.array.elems = NULL; + jbv.val.array.nElems = 0; + jbv.val.array.rawScalar = false; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Construct an empty object jsonb. + */ +Jsonb * +JsonbMakeEmptyObject(void) +{ + JsonbValue jbv; + + jbv.type = jbvObject; + jbv.val.object.pairs = NULL; + jbv.val.object.nPairs = 0; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Convert jsonb to a C-string stripping quotes from scalar strings. + */ +char * +JsonbUnquote(Jsonb *jb) +{ + if (JB_ROOT_IS_SCALAR(jb)) + { + JsonbValue v; + + (void) JsonbExtractScalar(&jb->root, &v); + + if (v.type == jbvString) + return pnstrdup(v.val.string.val, v.val.string.len); + else if (v.type == jbvBool) + return pstrdup(v.val.boolean ? "true" : "false"); + else if (v.type == jbvNumeric) + return DatumGetCString(DirectFunctionCall1(numeric_out, + PointerGetDatum(v.val.numeric))); + else if (v.type == jbvNull) + return pstrdup("null"); + else + { + elog(ERROR, "unrecognized jsonb value type %d", v.type); + return NULL; + } + } + else + return JsonbToCString(NULL, &jb->root, VARSIZE(jb)); +} diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index a4bfa5e404..2d5fa285fc 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -265,6 +265,7 @@ typedef struct PopulateArrayContext int *dims; /* dimensions */ int *sizes; /* current dimension counters */ int ndims; /* number of dimensions */ + Node *escontext; /* For soft-error capture */ } PopulateArrayContext; /* state for populate_array_json() */ @@ -442,12 +443,13 @@ static void JsValueToJsObject(JsValue *jsv, JsObject *jso); static Datum populate_composite(CompositeIOData *io, Oid typid, const char *colname, MemoryContext mcxt, HeapTupleHeader defaultval, JsValue *jsv, bool isnull); -static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv); +static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext); static void prepare_column_cache(ColumnIOData *column, Oid typid, int32 typmod, MemoryContext mcxt, bool need_scalar); static Datum populate_record_field(ColumnIOData *col, Oid typid, int32 typmod, const char *colname, MemoryContext mcxt, Datum defaultval, - JsValue *jsv, bool *isnull); + JsValue *jsv, bool *isnull, Node *escontext); static RecordIOData *allocate_record_info(MemoryContext mcxt, int ncolumns); static bool JsObjectGetField(JsObject *obj, char *field, JsValue *jsv); static void populate_recordset_record(PopulateRecordsetState *state, JsObject *obj); @@ -459,7 +461,8 @@ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims); static void populate_array_check_dimension(PopulateArrayContext *ctx, int ndim); static void populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv); static Datum populate_array(ArrayIOData *aio, const char *colname, - MemoryContext mcxt, JsValue *jsv); + MemoryContext mcxt, JsValue *jsv, Node *escontext, + bool *isnull); static Datum populate_domain(DomainIOData *io, Oid typid, const char *colname, MemoryContext mcxt, JsValue *jsv, bool isnull); @@ -2484,12 +2487,12 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) if (ndim <= 0) { if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the value of key \"%s\".", ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"))); } @@ -2506,13 +2509,13 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) appendStringInfo(&indices, "[%d]", ctx->sizes[i]); if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s of key \"%s\".", indices.data, ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s.", @@ -2520,7 +2523,11 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) } } -/* set the number of dimensions of the populated array when it becomes known */ +/* + * Set the number of dimensions of the populated array when it becomes known. + * + * Returns without doing anything if the input (ndims) is erratic. + */ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) { @@ -2531,6 +2538,10 @@ populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) if (ndims <= 0) populate_array_report_expected_array(ctx, ndims); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + ctx->ndims = ndims; ctx->dims = palloc(sizeof(int) * ndims); ctx->sizes = palloc0(sizeof(int) * ndims); @@ -2548,12 +2559,16 @@ populate_array_check_dimension(PopulateArrayContext *ctx, int ndim) if (ctx->dims[ndim] == -1) ctx->dims[ndim] = dim; /* assign dimension if not yet known */ else if (ctx->dims[ndim] != dim) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("malformed JSON array"), errdetail("Multidimensional arrays must have " "sub-arrays with matching dimensions."))); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + /* reset the current array dimension size counter */ ctx->sizes[ndim] = 0; @@ -2573,7 +2588,10 @@ populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv) ctx->aio->element_type, ctx->aio->element_typmod, NULL, ctx->mcxt, PointerGetDatum(NULL), - jsv, &element_isnull); + jsv, &element_isnull, ctx->escontext); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; accumArrayResult(ctx->astate, element, element_isnull, ctx->aio->element_type, ctx->acxt); @@ -2594,6 +2612,10 @@ populate_array_object_start(void *_state) else if (ndim < state->ctx->ndims) populate_array_report_expected_array(state->ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2611,6 +2633,10 @@ populate_array_array_end(void *_state) if (ndim < ctx->ndims) populate_array_check_dimension(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2686,6 +2712,10 @@ populate_array_scalar(void *_state, char *token, JsonTokenType tokentype) else if (ndim < ctx->ndims) populate_array_report_expected_array(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + if (ndim == ctx->ndims) { /* remember the scalar element token */ @@ -2715,7 +2745,13 @@ populate_array_json(PopulateArrayContext *ctx, char *json, int len) sem.array_element_end = populate_array_element_end; sem.scalar = populate_array_scalar; - pg_parse_json_or_ereport(state.lex, &sem); + if (!pg_parse_json_or_errsave(state.lex, &sem, ctx->escontext)) + { + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + pfree(state.lex); + return; + } /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2740,10 +2776,15 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ check_stack_depth(); - if (jbv->type != jbvBinary || !JsonContainerIsArray(jbc)) + if (jbv->type != jbvBinary || + !JsonContainerIsArray(jbc) || + JsonContainerIsScalar(jbc)) + { populate_array_report_expected_array(ctx, ndim - 1); - - Assert(!JsonContainerIsScalar(jbc)); + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + return; + } it = JsonbIteratorInit(jbc); @@ -2762,7 +2803,12 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ (tok == WJB_ELEM && (val.type != jbvBinary || !JsonContainerIsArray(val.val.binary.data))))) + { populate_array_assign_ndims(ctx, ndim); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + } jsv.is_json = false; jsv.val.jsonb = &val; @@ -2780,6 +2826,9 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ { /* populate child sub-array */ populate_array_dim_jsonb(ctx, &val, ndim + 1); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2797,12 +2846,18 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ Assert(tok == WJB_DONE && !it); } -/* recursively populate an array from json/jsonb */ +/* + * Recursively populate an array from json/jsonb + * + * *isnull is set to true if an error is reported during parsing. + */ static Datum populate_array(ArrayIOData *aio, const char *colname, MemoryContext mcxt, - JsValue *jsv) + JsValue *jsv, + Node *escontext, + bool *isnull) { PopulateArrayContext ctx; Datum result; @@ -2817,6 +2872,7 @@ populate_array(ArrayIOData *aio, ctx.ndims = 0; /* unknown yet */ ctx.dims = NULL; ctx.sizes = NULL; + ctx.escontext = escontext; if (jsv->is_json) populate_array_json(&ctx, jsv->val.json.str, @@ -2825,7 +2881,16 @@ populate_array(ArrayIOData *aio, else { populate_array_dim_jsonb(&ctx, jsv->val.jsonb, 1); - ctx.dims[0] = ctx.sizes[0]; + /* Nothing to do on an error. */ + if (!SOFT_ERROR_OCCURRED(ctx.escontext)) + ctx.dims[0] = ctx.sizes[0]; + } + + /* Nothing to return if an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx.escontext)) + { + *isnull = true; + return (Datum) 0; } Assert(ctx.ndims > 0); @@ -2842,6 +2907,7 @@ populate_array(ArrayIOData *aio, pfree(ctx.sizes); pfree(lbs); + *isnull = false; return result; } @@ -2957,7 +3023,8 @@ populate_composite(CompositeIOData *io, /* populate non-null scalar value from json/jsonb value */ static Datum -populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) +populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext) { Datum res; char *str = NULL; @@ -3028,7 +3095,11 @@ populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) elog(ERROR, "unrecognized jsonb type: %d", (int) jbv->type); } - res = InputFunctionCall(&io->typiofunc, str, io->typioparam, typmod); + if (!InputFunctionCallSafe(&io->typiofunc, str, io->typioparam, typmod, + escontext, &res)) + { + res = (Datum) 0; + } /* free temporary buffer */ if (str != json) @@ -3054,7 +3125,7 @@ populate_domain(DomainIOData *io, res = populate_record_field(io->base_io, io->base_typid, io->base_typmod, colname, mcxt, PointerGetDatum(NULL), - jsv, &isnull); + jsv, &isnull, NULL); Assert(!isnull); } @@ -3159,7 +3230,8 @@ populate_record_field(ColumnIOData *col, MemoryContext mcxt, Datum defaultval, JsValue *jsv, - bool *isnull) + bool *isnull, + Node *escontext) { TypeCat typcat; @@ -3192,10 +3264,12 @@ populate_record_field(ColumnIOData *col, switch (typcat) { case TYPECAT_SCALAR: - return populate_scalar(&col->scalar_io, typid, typmod, jsv); + return populate_scalar(&col->scalar_io, typid, typmod, jsv, + escontext); case TYPECAT_ARRAY: - return populate_array(&col->io.array, colname, mcxt, jsv); + return populate_array(&col->io.array, colname, mcxt, jsv, + escontext, isnull); case TYPECAT_COMPOSITE: case TYPECAT_COMPOSITE_DOMAIN: @@ -3216,6 +3290,53 @@ populate_record_field(ColumnIOData *col, } } +/* recursively populate specified type from a json/jsonb value */ +Datum +json_populate_type(Datum json_val, Oid json_type, Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext) +{ + JsValue jsv = {0}; + JsonbValue jbv; + + jsv.is_json = json_type == JSONOID; + + if (*isnull) + { + if (jsv.is_json) + jsv.val.json.str = NULL; + else + jsv.val.jsonb = NULL; + } + else if (jsv.is_json) + { + text *json = DatumGetTextPP(json_val); + + jsv.val.json.str = VARDATA_ANY(json); + jsv.val.json.len = VARSIZE_ANY_EXHDR(json); + jsv.val.json.type = JSON_TOKEN_INVALID; /* not used in + * populate_composite() */ + } + else + { + Jsonb *jsonb = DatumGetJsonbP(json_val); + + jsv.val.jsonb = &jbv; + + /* fill binary jsonb value pointing to jb */ + jbv.type = jbvBinary; + jbv.val.binary.data = &jsonb->root; + jbv.val.binary.len = VARSIZE(jsonb) - VARHDRSZ; + } + + if (!*cache) + *cache = MemoryContextAllocZero(mcxt, sizeof(ColumnIOData)); + + return populate_record_field(*cache, typid, typmod, NULL, mcxt, + PointerGetDatum(NULL), &jsv, isnull, + escontext); +} + static RecordIOData * allocate_record_info(MemoryContext mcxt, int ncolumns) { @@ -3357,7 +3478,8 @@ populate_record(TupleDesc tupdesc, mcxt, nulls[i] ? (Datum) 0 : values[i], &field, - &nulls[i]); + &nulls[i], + NULL); } res = heap_form_tuple(tupdesc, values, nulls); diff --git a/src/backend/utils/adt/jsonpath.c b/src/backend/utils/adt/jsonpath.c index 7891fde310..5194d0d91f 100644 --- a/src/backend/utils/adt/jsonpath.c +++ b/src/backend/utils/adt/jsonpath.c @@ -68,7 +68,9 @@ #include "libpq/pqformat.h" #include "nodes/miscnodes.h" #include "miscadmin.h" +#include "nodes/nodeFuncs.h" #include "utils/builtins.h" +#include "utils/formatting.h" #include "utils/json.h" #include "utils/jsonpath.h" @@ -1109,3 +1111,256 @@ jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, return true; } + +/* SQL/JSON datatype status: */ +typedef enum JsonPathDatatypeStatus +{ + jpdsNonDateTime, /* null, bool, numeric, string, array, object */ + jpdsUnknownDateTime, /* unknown datetime type */ + jpdsDateTimeZoned, /* timetz, timestamptz */ + jpdsDateTimeNonZoned /* time, timestamp, date */ +} JsonPathDatatypeStatus; + +/* Context for jspIsMutableWalker() */ +typedef struct JsonPathMutableContext +{ + List *varnames; /* list of variable names */ + List *varexprs; /* list of variable expressions */ + JsonPathDatatypeStatus current; /* status of @ item */ + bool lax; /* jsonpath is lax or strict */ + bool mutable; /* resulting mutability status */ +} JsonPathMutableContext; + +/* + * Recursive walker for jspIsMutable() + */ +static JsonPathDatatypeStatus +jspIsMutableWalker(JsonPathItem *jpi, JsonPathMutableContext *cxt) +{ + JsonPathItem next; + JsonPathDatatypeStatus status = jpdsNonDateTime; + + while (!cxt->mutable) + { + JsonPathItem arg; + JsonPathDatatypeStatus leftStatus; + JsonPathDatatypeStatus rightStatus; + + switch (jpi->type) + { + case jpiRoot: + Assert(status == jpdsNonDateTime); + break; + + case jpiCurrent: + Assert(status == jpdsNonDateTime); + status = cxt->current; + break; + + case jpiFilter: + { + JsonPathDatatypeStatus prevStatus = cxt->current; + + cxt->current = status; + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + + cxt->current = prevStatus; + break; + } + + case jpiVariable: + { + int32 len; + const char *name = jspGetString(jpi, &len); + ListCell *lc1; + ListCell *lc2; + + Assert(status == jpdsNonDateTime); + + forboth(lc1, cxt->varnames, lc2, cxt->varexprs) + { + String *varname = lfirst_node(String, lc1); + Node *varexpr = lfirst(lc2); + + if (strncmp(varname->sval, name, len)) + continue; + + switch (exprType(varexpr)) + { + case DATEOID: + case TIMEOID: + case TIMESTAMPOID: + status = jpdsDateTimeNonZoned; + break; + + case TIMETZOID: + case TIMESTAMPTZOID: + status = jpdsDateTimeZoned; + break; + + default: + status = jpdsNonDateTime; + break; + } + + break; + } + break; + } + + case jpiEqual: + case jpiNotEqual: + case jpiLess: + case jpiGreater: + case jpiLessOrEqual: + case jpiGreaterOrEqual: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + leftStatus = jspIsMutableWalker(&arg, cxt); + + jspGetRightArg(jpi, &arg); + rightStatus = jspIsMutableWalker(&arg, cxt); + + /* + * Comparison of datetime type with different timezone status + * is mutable. + */ + if (leftStatus != jpdsNonDateTime && + rightStatus != jpdsNonDateTime && + (leftStatus == jpdsUnknownDateTime || + rightStatus == jpdsUnknownDateTime || + leftStatus != rightStatus)) + cxt->mutable = true; + break; + + case jpiNot: + case jpiIsUnknown: + case jpiExists: + case jpiPlus: + case jpiMinus: + Assert(status == jpdsNonDateTime); + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiAnd: + case jpiOr: + case jpiAdd: + case jpiSub: + case jpiMul: + case jpiDiv: + case jpiMod: + case jpiStartsWith: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + jspGetRightArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiIndexArray: + for (int i = 0; i < jpi->content.array.nelems; i++) + { + JsonPathItem from; + JsonPathItem to; + + if (jspGetArraySubscript(jpi, &from, &to, i)) + jspIsMutableWalker(&to, cxt); + + jspIsMutableWalker(&from, cxt); + } + /* FALLTHROUGH */ + + case jpiAnyArray: + if (!cxt->lax) + status = jpdsNonDateTime; + break; + + case jpiAny: + if (jpi->content.anybounds.first > 0) + status = jpdsNonDateTime; + break; + + case jpiDatetime: + if (jpi->content.arg) + { + char *template; + + jspGetArg(jpi, &arg); + if (arg.type != jpiString) + { + status = jpdsNonDateTime; + break; /* there will be runtime error */ + } + + template = jspGetString(&arg, NULL); + if (datetime_format_has_tz(template)) + status = jpdsDateTimeZoned; + else + status = jpdsDateTimeNonZoned; + } + else + { + status = jpdsUnknownDateTime; + } + break; + + case jpiLikeRegex: + Assert(status == jpdsNonDateTime); + jspInitByBuffer(&arg, jpi->base, jpi->content.like_regex.expr); + jspIsMutableWalker(&arg, cxt); + break; + + /* literals */ + case jpiNull: + case jpiString: + case jpiNumeric: + case jpiBool: + /* accessors */ + case jpiKey: + case jpiAnyKey: + /* special items */ + case jpiSubscript: + case jpiLast: + /* item methods */ + case jpiType: + case jpiSize: + case jpiAbs: + case jpiFloor: + case jpiCeiling: + case jpiDouble: + case jpiKeyValue: + status = jpdsNonDateTime; + break; + } + + if (!jspGetNext(jpi, &next)) + break; + + jpi = &next; + } + + return status; +} + +/* + * Check whether jsonpath expression is immutable or not. + */ +bool +jspIsMutable(JsonPath *path, List *varnames, List *varexprs) +{ + JsonPathMutableContext cxt; + JsonPathItem jpi; + + cxt.varnames = varnames; + cxt.varexprs = varexprs; + cxt.current = jpdsNonDateTime; + cxt.lax = (path->header & JSONPATH_LAX) != 0; + cxt.mutable = false; + + jspInit(&jpi, path); + jspIsMutableWalker(&jpi, &cxt); + + return cxt.mutable; +} diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index 2d0599b4aa..eded22e194 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -87,12 +87,16 @@ typedef struct JsonBaseObjectInfo int id; } JsonBaseObjectInfo; +typedef int (*JsonPathVarCallback) (void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); + /* * Context of jsonpath execution. */ typedef struct JsonPathExecContext { - Jsonb *vars; /* variables to substitute into jsonpath */ + void *vars; /* variables to substitute into jsonpath */ + JsonPathVarCallback getVar; JsonbValue *root; /* for $ evaluation */ JsonbValue *current; /* for @ evaluation */ JsonBaseObjectInfo baseObject; /* "base object" for .keyvalue() @@ -174,7 +178,8 @@ typedef JsonPathBool (*JsonPathPredicateCallback) (JsonPathItem *jsp, void *param); typedef Numeric (*BinaryArithmFunc) (Numeric num1, Numeric num2, bool *error); -static JsonPathExecResult executeJsonPath(JsonPath *path, Jsonb *vars, +static JsonPathExecResult executeJsonPath(JsonPath *path, void *vars, + JsonPathVarCallback getVar, Jsonb *json, bool throwErrors, JsonValueList *result, bool useTz); static JsonPathExecResult executeItem(JsonPathExecContext *cxt, @@ -225,8 +230,13 @@ static JsonPathExecResult appendBoolResult(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonValueList *found, JsonPathBool res); static void getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, JsonbValue *value); +static int GetJsonPathVar(void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); static void getJsonPathVariable(JsonPathExecContext *cxt, - JsonPathItem *variable, Jsonb *vars, JsonbValue *value); + JsonPathItem *variable, JsonbValue *value); +static int getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, + int varNameLen, JsonbValue *val, + JsonbValue *baseObject); static int JsonbArraySize(JsonbValue *jb); static JsonPathBool executeComparison(JsonPathItem *cmp, JsonbValue *lv, JsonbValue *rv, void *p); @@ -284,7 +294,8 @@ jsonb_path_exists_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - res = executeJsonPath(jp, vars, jb, !silent, NULL, tz); + res = executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, NULL, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -339,7 +350,8 @@ jsonb_path_match_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -417,7 +429,8 @@ jsonb_path_query_internal(FunctionCallInfo fcinfo, bool tz) vars = PG_GETARG_JSONB_P_COPY(2); silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); funcctx->user_fctx = JsonValueListGetList(&found); @@ -464,7 +477,8 @@ jsonb_path_query_array_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_RETURN_JSONB_P(JsonbValueToJsonb(wrapItemsInArray(&found))); } @@ -495,7 +509,8 @@ jsonb_path_query_first_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); if (JsonValueListLength(&found) >= 1) PG_RETURN_JSONB_P(JsonbValueToJsonb(JsonValueListHead(&found))); @@ -537,8 +552,9 @@ jsonb_path_query_first_tz(PG_FUNCTION_ARGS) * In other case it tries to find all the satisfied result items. */ static JsonPathExecResult -executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, - JsonValueList *result, bool useTz) +executeJsonPath(JsonPath *path, void *vars, JsonPathVarCallback getVar, + Jsonb *json, bool throwErrors, JsonValueList *result, + bool useTz) { JsonPathExecContext cxt; JsonPathExecResult res; @@ -550,22 +566,16 @@ executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, if (!JsonbExtractScalar(&json->root, &jbv)) JsonbInitBinary(&jbv, json); - if (vars && !JsonContainerIsObject(&vars->root)) - { - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"vars\" argument is not an object"), - errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); - } - cxt.vars = vars; + cxt.getVar = getVar; cxt.laxMode = (path->header & JSONPATH_LAX) != 0; cxt.ignoreStructuralErrors = cxt.laxMode; cxt.root = &jbv; cxt.current = &jbv; cxt.baseObject.jbc = NULL; cxt.baseObject.id = 0; - cxt.lastGeneratedObjectId = vars ? 2 : 1; + /* 1 + number of base objects in vars */ + cxt.lastGeneratedObjectId = 1 + getVar(vars, NULL, 0, NULL, NULL); cxt.innermostArraySize = -1; cxt.throwErrors = throwErrors; cxt.useTz = useTz; @@ -2108,54 +2118,118 @@ getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, &value->val.string.len); break; case jpiVariable: - getJsonPathVariable(cxt, item, cxt->vars, value); + getJsonPathVariable(cxt, item, value); return; default: elog(ERROR, "unexpected jsonpath item type"); } } +/* + * Returns the computed value of a JSON path variable with given name. + */ +static int +GetJsonPathVar(void *cxt, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject) +{ + JsonPathVariable *var = NULL; + List *vars = cxt; + ListCell *lc; + int id = 1; + + if (!varName) + return list_length(vars); + + foreach(lc, vars) + { + JsonPathVariable *curvar = lfirst(lc); + + if (!strncmp(curvar->name, varName, varNameLen)) + { + var = curvar; + break; + } + + id++; + } + + if (!var) + return -1; + + if (var->isnull) + { + val->type = jbvNull; + return 0; + } + + JsonItemFromDatum(var->value, var->typid, var->typmod, val); + + *baseObject = *val; + return id; +} + /* * Get the value of variable passed to jsonpath executor */ static void getJsonPathVariable(JsonPathExecContext *cxt, JsonPathItem *variable, - Jsonb *vars, JsonbValue *value) + JsonbValue *value) { char *varName; int varNameLength; + JsonbValue baseObject; + int baseObjectId; + + Assert(variable->type == jpiVariable); + varName = jspGetString(variable, &varNameLength); + + if (!cxt->vars || + (baseObjectId = cxt->getVar(cxt->vars, varName, varNameLength, value, + &baseObject)) < 0) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("could not find jsonpath variable \"%s\"", + pnstrdup(varName, varNameLength)))); + + if (baseObjectId > 0) + setBaseObject(cxt, &baseObject, baseObjectId); +} + +static int +getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, int varNameLength, + JsonbValue *value, JsonbValue *baseObject) +{ + Jsonb *vars = varsJsonb; JsonbValue tmp; JsonbValue *v; - if (!vars) + if (!varName) { - value->type = jbvNull; - return; + if (vars && !JsonContainerIsObject(&vars->root)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"vars\" argument is not an object"), + errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); + } + + return vars ? 1 : 0; /* count of base objects */ } - Assert(variable->type == jpiVariable); - varName = jspGetString(variable, &varNameLength); tmp.type = jbvString; tmp.val.string.val = varName; tmp.val.string.len = varNameLength; v = findJsonbValueFromContainer(&vars->root, JB_FOBJECT, &tmp); - if (v) - { - *value = *v; - pfree(v); - } - else - { - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("could not find jsonpath variable \"%s\"", - pnstrdup(varName, varNameLength)))); - } + if (!v) + return -1; - JsonbInitBinary(&tmp, vars); - setBaseObject(cxt, &tmp, 1); + *value = *v; + pfree(v); + + JsonbInitBinary(baseObject, vars); + return 1; } /**************** Support functions for JsonPath execution *****************/ @@ -2812,3 +2886,240 @@ compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, return DatumGetInt32(DirectFunctionCall2(cmpfunc, val1, val2)); } + +/* Executor-callable JSON_EXISTS implementation */ +bool +JsonPathExists(Datum jb, JsonPath *jp, List *vars, bool *error) +{ + JsonPathExecResult res = executeJsonPath(jp, vars, GetJsonPathVar, + DatumGetJsonbP(jb), !error, NULL, + true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + *error = true; + + return res == jperOk; +} + +/* Executor-callable JSON_QUERY implementation */ +Datum +JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, bool *empty, + bool *error, List *vars) +{ + JsonbValue *first; + bool wrap; + JsonValueList found = {0}; + JsonPathExecResult res PG_USED_FOR_ASSERTS_ONLY; + int count; + + res = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + { + *error = true; + *empty = false; + return (Datum) 0; + } + + count = JsonValueListLength(&found); + + first = count ? JsonValueListHead(&found) : NULL; + + if (!first) + wrap = false; + else if (wrapper == JSW_NONE) + wrap = false; + else if (wrapper == JSW_UNCONDITIONAL) + wrap = true; + else if (wrapper == JSW_CONDITIONAL) + wrap = count > 1 || + IsAJsonbScalar(first) || + (first->type == jbvBinary && + JsonContainerIsScalar(first->val.binary.data)); + else + { + elog(ERROR, "unrecognized json wrapper %d", wrapper); + wrap = false; + } + + if (wrap) + return JsonbPGetDatum(JsonbValueToJsonb(wrapItemsInArray(&found))); + + if (count > 1) + { + if (error) + { + *error = true; + return (Datum) 0; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_QUERY should return singleton item without wrapper"), + errhint("Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array."))); + } + + if (first) + return JsonbPGetDatum(JsonbValueToJsonb(first)); + + *empty = true; + return PointerGetDatum(NULL); +} + +/* Executor-callable JSON_VALUE implementation */ +JsonbValue * +JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars) +{ + JsonbValue *res; + JsonValueList found = {0}; + JsonPathExecResult jper PG_USED_FOR_ASSERTS_ONLY; + int count; + + jper = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(jper)); + + if (error && jperIsError(jper)) + { + *error = true; + *empty = false; + return NULL; + } + + count = JsonValueListLength(&found); + + *empty = !count; + + if (*empty) + return NULL; + + if (count > 1) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + res = JsonValueListHead(&found); + + if (res->type == jbvBinary && + JsonContainerIsScalar(res->val.binary.data)) + JsonbExtractScalar(res->val.binary.data, res); + + if (!IsAJsonbScalar(res)) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_SCALAR_REQUIRED), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + if (res->type == jbvNull) + return NULL; + + return res; +} + +static void +JsonbValueInitNumericDatum(JsonbValue *jbv, Datum num) +{ + jbv->type = jbvNumeric; + jbv->val.numeric = DatumGetNumeric(num); +} + +void +JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) +{ + switch (typid) + { + case BOOLOID: + res->type = jbvBool; + res->val.boolean = DatumGetBool(val); + break; + case NUMERICOID: + JsonbValueInitNumericDatum(res, val); + break; + case INT2OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int2_numeric, val)); + break; + case INT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int4_numeric, val)); + break; + case INT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int8_numeric, val)); + break; + case FLOAT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float4_numeric, val)); + break; + case FLOAT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float8_numeric, val)); + break; + case TEXTOID: + case VARCHAROID: + res->type = jbvString; + res->val.string.val = VARDATA_ANY(val); + res->val.string.len = VARSIZE_ANY_EXHDR(val); + break; + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + res->type = jbvDatetime; + res->val.datetime.value = val; + res->val.datetime.typid = typid; + res->val.datetime.typmod = typmod; + res->val.datetime.tz = 0; + break; + case JSONBOID: + { + JsonbValue *jbv = res; + Jsonb *jb = DatumGetJsonbP(val); + + if (JsonContainerIsScalar(&jb->root)) + { + bool result PG_USED_FOR_ASSERTS_ONLY; + + result = JsonbExtractScalar(&jb->root, jbv); + Assert(result); + } + else + JsonbInitBinary(jbv, jb); + break; + } + case JSONOID: + { + text *txt = DatumGetTextP(val); + char *str = text_to_cstring(txt); + Jsonb *jb; + + jb = DatumGetJsonbP(DirectFunctionCall1(jsonb_in, + CStringGetDatum(str))); + pfree(str); + + JsonItemFromDatum(JsonbPGetDatum(jb), JSONBOID, -1, res); + break; + } + default: + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); + } +} diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d1b03d6cb2..d1ec418ccf 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -476,6 +476,8 @@ static void get_const_expr(Const *constval, deparse_context *context, int showtype); static void get_const_collation(Const *constval, deparse_context *context); static void get_json_format(JsonFormat *format, StringInfo buf); +static void get_json_returning(JsonReturning *returning, StringInfo buf, + bool json_format_by_default); static void get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, bool showimplicit); static void get_json_constructor_options(JsonConstructorExpr *ctor, @@ -518,6 +520,8 @@ static char *generate_qualified_type_name(Oid typid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); +static void get_json_path_spec(Node *path_spec, deparse_context *context, + bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8279,6 +8283,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_WindowFunc: case T_FuncExpr: case T_JsonConstructorExpr: + case T_JsonExpr: /* function-like: name(..) or name[..] */ return true; @@ -8450,6 +8455,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_GroupingFunc: /* own parentheses */ case T_WindowFunc: /* own parentheses */ case T_CaseExpr: /* other separators */ + case T_JsonExpr: /* own parentheses */ return true; default: return false; @@ -8565,6 +8571,65 @@ get_rule_expr_paren(Node *node, deparse_context *context, appendStringInfoChar(context->buf, ')'); } +static void +get_json_behavior(JsonBehavior *behavior, deparse_context *context, + const char *on) +{ + /* + * The order of array elements must correspond to the order of + * JsonBehaviorType members. + */ + const char *behavior_names[] = + { + " NULL", + " ERROR", + " EMPTY", + " TRUE", + " FALSE", + " UNKNOWN", + " EMPTY ARRAY", + " EMPTY OBJECT", + " DEFAULT " + }; + + if ((int) behavior->btype < 0 || behavior->btype >= lengthof(behavior_names)) + elog(ERROR, "invalid json behavior type: %d", behavior->btype); + + appendStringInfoString(context->buf, behavior_names[behavior->btype]); + + if (behavior->btype == JSON_BEHAVIOR_DEFAULT) + get_rule_expr(behavior->default_expr, context, false); + + appendStringInfo(context->buf, " ON %s", on); +} + +/* + * get_json_expr_options + * + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + */ +static void +get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, + JsonBehaviorType default_behavior) +{ + if (jsexpr->op == JSON_QUERY_OP) + { + if (jsexpr->wrapper == JSW_CONDITIONAL) + appendStringInfo(context->buf, " WITH CONDITIONAL WRAPPER"); + else if (jsexpr->wrapper == JSW_UNCONDITIONAL) + appendStringInfo(context->buf, " WITH UNCONDITIONAL WRAPPER"); + + if (jsexpr->omit_quotes) + appendStringInfo(context->buf, " OMIT QUOTES"); + } + + if (jsexpr->op != JSON_EXISTS_OP && + jsexpr->on_empty->btype != default_behavior) + get_json_behavior(jsexpr->on_empty, context, "EMPTY"); + + if (jsexpr->on_error->btype != default_behavior) + get_json_behavior(jsexpr->on_error, context, "ERROR"); +} /* ---------- * get_rule_expr - Parse back an expression @@ -9724,6 +9789,7 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonValueExpr: { JsonValueExpr *jve = (JsonValueExpr *) node; @@ -9773,6 +9839,63 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + appendStringInfoString(buf, "JSON_QUERY("); + break; + case JSON_VALUE_OP: + appendStringInfoString(buf, "JSON_VALUE("); + break; + case JSON_EXISTS_OP: + appendStringInfoString(buf, "JSON_EXISTS("); + break; + } + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_json_path_spec(jexpr->path_spec, context, showimplicit); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoString(buf, " PASSING "); + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + get_rule_expr((Node *) lfirst(lc2), context, showimplicit); + appendStringInfo(buf, " AS %s", + ((String *) lfirst_node(String, lc1))->sval); + } + } + + if (jexpr->op != JSON_EXISTS_OP || + jexpr->returning->typid != BOOLOID) + get_json_returning(jexpr->returning, context->buf, + jexpr->op == JSON_QUERY_OP); + + get_json_expr_options(jexpr, context, + jexpr->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : JSON_BEHAVIOR_NULL); + + appendStringInfoString(buf, ")"); + } + break; + case T_List: { char *sep; @@ -9896,6 +10019,7 @@ looks_like_function(Node *node) case T_MinMaxExpr: case T_SQLValueFunction: case T_XmlExpr: + case T_JsonExpr: /* these are all accepted by func_expr_common_subexpr */ return true; default: @@ -10755,6 +10879,18 @@ get_const_collation(Const *constval, deparse_context *context) } } +/* + * get_json_path_spec - Parse back a JSON path specification + */ +static void +get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit) +{ + if (IsA(path_spec, Const)) + get_const_expr((Const *) path_spec, context, -1); + else + get_rule_expr(path_spec, context, showimplicit); +} + /* * get_json_format - Parse back a JsonFormat node */ diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index 048573c2bc..69fb577850 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -16,12 +16,15 @@ #include "executor/nodeAgg.h" #include "nodes/execnodes.h" +#include "nodes/miscnodes.h" /* forward references to avoid circularity */ struct ExprEvalStep; struct SubscriptingRefState; struct ScalarArrayOpExprHashTable; struct JsonConstructorExprState; +struct JsonbValue; +struct JsonExprState; /* Bits in ExprState->flags (see also execnodes.h for public flag bits): */ /* expression's interpreter has been initialized */ @@ -238,6 +241,11 @@ typedef enum ExprEvalOp EEOP_XMLEXPR, EEOP_JSON_CONSTRUCTOR, EEOP_IS_JSON, + EEOP_JSONEXPR_SKIP, + EEOP_JSONEXPR_PATH, + EEOP_JSONEXPR_BEHAVIOR, + EEOP_JSONEXPR_COERCION, + EEOP_JSONEXPR_COERCION_FINISH, EEOP_AGGREF, EEOP_GROUPING_FUNC, EEOP_WINDOW_FUNC, @@ -689,6 +697,57 @@ typedef struct ExprEvalStep JsonIsPredicate *pred; /* original expression node */ } is_json; + /* for EEOP_JSONEXPR_PATH */ + struct + { + struct JsonExprState *jsestate; + } jsonexpr; + + /* for EEOP_JSONEXPR_SKIP */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprSkip() */ + int jump_coercion; + int jump_passing_args; + } jsonexpr_skip; + + /* for EEOP_JSONEXPR_BEHAVIOR */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprBehavior() */ + int jump_onerror_expr; + int jump_onempty_expr; + int jump_coercion; + int jump_skip_coercion; + } jsonexpr_behavior; + + /* for EEOP_JSONEXPR_COERCION */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion; + + /* for EEOP_JSONEXPR_COERCION_FINISH */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion_finish; } d; } ExprEvalStep; @@ -752,6 +811,111 @@ typedef struct JsonConstructorExprState int nargs; } JsonConstructorExprState; +/* + * Information computed before evaluating a JsonExpr expression. + */ +typedef struct JsonExprPreEvalState +{ + /* value/isnull for JsonExpr.formatted_expr */ + NullableDatum formatted_expr; + + /* value/isnull for JsonExpr.pathspec */ + NullableDatum pathspec; + + /* JsonPathVariable entries for JsonExpr.passing_values */ + List *args; +} JsonExprPreEvalState; + +/* + * State for a given member of JsonItemCoercions. + */ +typedef struct JsonCoercionState +{ + /* Expression used to evaluate the coercion */ + JsonCoercion *coercion; + + /* ExprEvalStep to compute this coercion's expression */ + int jump_eval_expr; +} JsonCoercionState; + +/* + * State for evaluating the coercion for a given JSON item using one of + * the following coercions. + * + * Note that while ExecInitExprRec() for JsonItemCoercions will initialize + * ExprEvalSteps for all of the members that need it, only one will get run + * during a given evaluation of the enclosing JsonExpr depending on the type + * of the result JSON item. + */ +typedef struct JsonItemCoercionsState +{ + JsonCoercionState *null; + JsonCoercionState *string; + JsonCoercionState *numeric; + JsonCoercionState *boolean; + JsonCoercionState *date; + JsonCoercionState *time; + JsonCoercionState *timetz; + JsonCoercionState *timestamp; + JsonCoercionState *timestamptz; + JsonCoercionState *composite; +} JsonItemCoercionsState; + +/* + * State for some post-JsonExpr-evaluation processing steps that gets filled + * in JsonExpr evaluation. + */ +typedef struct JsonExprPostEvalState +{ + /* Is JSON item empty? */ + bool empty; + + /* Did JSON item evaluation cause an error? */ + bool error; + + /* Cache for json_populate_type() called for coercion in some cases */ + void *cache; + + /* + * State for evaluating a JSON item coercion. Points to one of those in + * JsonExprState.coercions chosen by ExecPrepareJsonItemCoercion(). + */ + JsonCoercionState *item_jcstate; + bool coercion_error; + bool coercion_done; + + ErrorSaveContext escontext; +} JsonExprPostEvalState; + +/* EEOP_JSONEXPR state, too big to inline */ +typedef struct JsonExprState +{ + /* original expression node */ + JsonExpr *jsexpr; + + /* + * Should errors be thrown or handled softly? Different parts of + * evaluating a given JsonExpr may require different treatment depending + * on the JsonExprOp; see ExecEvalJsonExprBehavior(). + */ + bool throw_error; + + JsonExprPreEvalState pre_eval; + JsonExprPostEvalState post_eval; + + struct + { + FmgrInfo *finfo; /* typinput function for output type */ + Oid typioparam; + } input; /* I/O info for output type */ + + /* + * Either of the following two is used by ExecEvalJsonExprCoercion() to + * apply coercion to the final result if needed. + */ + JsonCoercionState *result_jcstate; + JsonItemCoercionsState *item_jcstates; +} JsonExprState; /* functions in execExpr.c */ extern void ExprEvalPushStep(ExprState *es, const ExprEvalStep *s); @@ -805,6 +969,14 @@ extern void ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op); extern void ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull); +extern int ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op); +extern void ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, + ExprContext *econtext); extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op); extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op, ExprContext *econtext); diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index c677e490d7..709ad967ba 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -17,6 +17,7 @@ #include "executor/execdesc.h" #include "fmgr.h" #include "nodes/lockoptions.h" +#include "nodes/miscnodes.h" #include "nodes/parsenodes.h" #include "utils/memutils.h" diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index cb714f4a19..584bf7001a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -129,6 +129,9 @@ typedef struct ExprState Datum *innermost_domainval; bool *innermost_domainnull; + + /* ErrorSaveContext for soft-error capture. */ + Node *escontext; } ExprState; diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 06d991b725..5e149b0266 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -111,6 +111,7 @@ extern VacuumRelation *makeVacuumRelation(RangeVar *relation, Oid oid, List *va_ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *expr, JsonFormat *format); +extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index d926713bd9..7d63a37d5f 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1727,6 +1727,12 @@ typedef enum JsonQuotes JS_QUOTES_OMIT /* OMIT QUOTES */ } JsonQuotes; +/* + * JsonPathSpec - + * representation of JSON path constant + */ +typedef char *JsonPathSpec; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1738,6 +1744,48 @@ typedef struct JsonOutput JsonReturning *returning; /* RETURNING FORMAT clause and type Oids */ } JsonOutput; +/* + * JsonArgument - + * representation of argument from JSON PASSING clause + */ +typedef struct JsonArgument +{ + NodeTag type; + JsonValueExpr *val; /* argument value expression */ + char *name; /* argument name */ +} JsonArgument; + +/* + * JsonCommon - + * representation of common syntax of functions using JSON path + */ +typedef struct JsonCommon +{ + NodeTag type; + JsonValueExpr *expr; /* context item expression */ + Node *pathspec; /* JSON path specification expression */ + char *pathname; /* path name, if any */ + List *passing; /* list of PASSING clause arguments, if any */ + int location; /* token location, or -1 if unknown */ +} JsonCommon; + +/* + * JsonFuncExpr - + * untransformed representation of JSON function expressions + */ +typedef struct JsonFuncExpr +{ + NodeTag type; + JsonExprOp op; /* expression type */ + JsonCommon *common; /* common syntax */ + JsonOutput *output; /* output clause, if specified */ + JsonBehavior *on_empty; /* ON EMPTY behavior, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + JsonWrapper wrapper; /* array wrapper behavior (JSON_QUERY only) */ + bool omit_quotes; /* omit or keep quotes? (JSON_QUERY only) */ + int location; /* token location, or -1 if unknown */ +} JsonFuncExpr; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 85e484bf43..3937114743 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1544,6 +1544,17 @@ typedef struct XmlExpr int location; } XmlExpr; +/* + * JsonExprOp - + * enumeration of JSON functions using JSON path + */ +typedef enum JsonExprOp +{ + JSON_VALUE_OP, /* JSON_VALUE() */ + JSON_QUERY_OP, /* JSON_QUERY() */ + JSON_EXISTS_OP /* JSON_EXISTS() */ +} JsonExprOp; + /* * JsonEncoding - * representation of JSON ENCODING clause @@ -1568,6 +1579,37 @@ typedef enum JsonFormatType * jsonb */ } JsonFormatType; +/* + * JsonBehaviorType - + * enumeration of behavior types used in JSON ON ... BEHAVIOR clause + * + * If enum members are reordered, get_json_behavior() from ruleutils.c + * must be updated accordingly. + */ +typedef enum JsonBehaviorType +{ + JSON_BEHAVIOR_NULL = 0, + JSON_BEHAVIOR_ERROR, + JSON_BEHAVIOR_EMPTY, + JSON_BEHAVIOR_TRUE, + JSON_BEHAVIOR_FALSE, + JSON_BEHAVIOR_UNKNOWN, + JSON_BEHAVIOR_EMPTY_ARRAY, + JSON_BEHAVIOR_EMPTY_OBJECT, + JSON_BEHAVIOR_DEFAULT +} JsonBehaviorType; + +/* + * JsonWrapper - + * representation of WRAPPER clause for JSON_QUERY() + */ +typedef enum JsonWrapper +{ + JSW_NONE, + JSW_CONDITIONAL, + JSW_UNCONDITIONAL, +} JsonWrapper; + /* * JsonFormat - * representation of JSON FORMAT clause @@ -1661,6 +1703,73 @@ typedef struct JsonIsPredicate int location; /* token location, or -1 if unknown */ } JsonIsPredicate; +/* + * JsonBehavior - + * representation of JSON ON ... BEHAVIOR clause + */ +typedef struct JsonBehavior +{ + NodeTag type; + JsonBehaviorType btype; /* behavior type */ + Node *default_expr; /* default expression, if any */ +} JsonBehavior; + +/* + * JsonCoercion - + * coercion from SQL/JSON item types to SQL types + */ +typedef struct JsonCoercion +{ + NodeTag type; + Node *expr; /* resulting expression coerced to target type */ + bool via_populate; /* coerce result using json_populate_type()? */ + bool via_io; /* coerce result using type input function? */ + Oid collation; /* collation for coercion via I/O or populate */ +} JsonCoercion; + +/* + * JsonItemCoercions - + * expressions for coercion from SQL/JSON item types directly to the + * output SQL type + */ +typedef struct JsonItemCoercions +{ + NodeTag type; + JsonCoercion *null; + JsonCoercion *string; + JsonCoercion *numeric; + JsonCoercion *boolean; + JsonCoercion *date; + JsonCoercion *time; + JsonCoercion *timetz; + JsonCoercion *timestamp; + JsonCoercion *timestamptz; + JsonCoercion *composite; /* arrays and objects */ +} JsonItemCoercions; + +/* + * JsonExpr - + * transformed representation of JSON_VALUE(), JSON_QUERY(), JSON_EXISTS() + */ +typedef struct JsonExpr +{ + Expr xpr; + JsonExprOp op; /* json function ID */ + Node *formatted_expr; /* formatted context item expression */ + JsonCoercion *result_coercion; /* resulting coercion to RETURNING type */ + JsonFormat *format; /* context item format (JSON/JSONB) */ + Node *path_spec; /* JSON path specification expression */ + List *passing_names; /* PASSING argument names */ + List *passing_values; /* PASSING argument values */ + JsonReturning *returning; /* RETURNING clause type/format info */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + JsonItemCoercions *coercions; /* coercions for JSON_VALUE */ + JsonWrapper wrapper; /* WRAPPER for JSON_QUERY */ + bool omit_quotes; /* KEEP/OMIT QUOTES for JSON_QUERY */ + int location; /* token location, or -1 if unknown */ +} JsonExpr; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 5984dcfa4b..0954d9fc7b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -93,6 +93,7 @@ PG_KEYWORD("commit", COMMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("committed", COMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("compression", COMPRESSION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("concurrently", CONCURRENTLY, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("conditional", CONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("configuration", CONFIGURATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("conflict", CONFLICT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("connection", CONNECTION, UNRESERVED_KEYWORD, BARE_LABEL) @@ -147,11 +148,13 @@ PG_KEYWORD("double", DOUBLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("drop", DROP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("each", EACH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("empty", EMPTY_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("event", EVENT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("except", EXCEPT, RESERVED_KEYWORD, AS_LABEL) @@ -233,10 +236,14 @@ PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_exists", JSON_EXISTS, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) @@ -302,6 +309,7 @@ PG_KEYWORD("off", OFF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("offset", OFFSET, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("oids", OIDS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("old", OLD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("omit", OMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("on", ON, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("only", ONLY, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("operator", OPERATOR, UNRESERVED_KEYWORD, BARE_LABEL) @@ -344,6 +352,7 @@ PG_KEYWORD("procedures", PROCEDURES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("publication", PUBLICATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("quotes", QUOTES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("range", RANGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("read", READ, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("real", REAL, COL_NAME_KEYWORD, BARE_LABEL) @@ -414,6 +423,7 @@ PG_KEYWORD("stdout", STDOUT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("storage", STORAGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("stored", STORED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("string", STRING_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("subscription", SUBSCRIPTION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("substring", SUBSTRING, COL_NAME_KEYWORD, BARE_LABEL) @@ -449,6 +459,7 @@ PG_KEYWORD("types", TYPES_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uescape", UESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unbounded", UNBOUNDED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uncommitted", UNCOMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("unconditional", UNCONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unencrypted", UNENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("union", UNION, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("unique", UNIQUE, RESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/formatting.h b/src/include/utils/formatting.h index 0cad3a2709..d4ca0f42ff 100644 --- a/src/include/utils/formatting.h +++ b/src/include/utils/formatting.h @@ -17,7 +17,6 @@ #ifndef _FORMATTING_H_ #define _FORMATTING_H_ - extern char *str_tolower(const char *buff, size_t nbytes, Oid collid); extern char *str_toupper(const char *buff, size_t nbytes, Oid collid); extern char *str_initcap(const char *buff, size_t nbytes, Oid collid); @@ -29,5 +28,6 @@ extern char *asc_initcap(const char *buff, size_t nbytes); extern Datum parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, Oid *typid, int32 *typmod, int *tz, struct Node *escontext); +extern bool datetime_format_has_tz(const char *fmt_str); #endif diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index f928e6142a..e628d4fdd0 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -422,6 +422,9 @@ extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, int estimated_len); +extern Jsonb *JsonbMakeEmptyArray(void); +extern Jsonb *JsonbMakeEmptyObject(void); +extern char *JsonbUnquote(Jsonb *jb); extern bool JsonbExtractScalar(JsonbContainer *jbc, JsonbValue *res); extern const char *JsonbTypeName(JsonbValue *val); diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index 7e9203b109..68b6c69cbb 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -15,6 +15,7 @@ #define JSONFUNCS_H #include "common/jsonapi.h" +#include "nodes/nodes.h" #include "utils/jsonb.h" /* @@ -86,5 +87,9 @@ extern Datum to_json_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid); extern Datum to_jsonb_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid); +extern Datum json_populate_type(Datum json_val, Oid json_type, + Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext); #endif diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index f0181e045f..4dae78a98c 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -16,7 +16,9 @@ #include "fmgr.h" #include "nodes/pg_list.h" +#include "nodes/primnodes.h" #include "utils/jsonb.h" +#include "utils/jsonfuncs.h" typedef struct { @@ -184,6 +186,7 @@ extern bool jspGetBool(JsonPathItem *v); extern char *jspGetString(JsonPathItem *v, int32 *len); extern bool jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, int i); +extern bool jspIsMutable(JsonPath *path, List *varnames, List *varexprs); extern const char *jspOperationName(JsonPathItemType type); @@ -261,4 +264,28 @@ extern bool jspConvertRegexFlags(uint32 xflags, int *result, struct Node *escontext); +/* + * Evaluation of jsonpath + */ + +/* External variable passed into jsonpath. */ +typedef struct JsonPathVariable +{ + char *name; + Oid typid; + int32 typmod; + Datum value; + bool isnull; +} JsonPathVariable; + +/* SQL/JSON item */ +extern void JsonItemFromDatum(Datum val, Oid typid, int32 typmod, + JsonbValue *res); + +extern bool JsonPathExists(Datum jb, JsonPath *path, List *vars, bool *error); +extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, + bool *empty, bool *error, List *vars); +extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, + bool *error, List *vars); + #endif diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer index 435c139ec2..b2aa44f36d 100644 --- a/src/interfaces/ecpg/preproc/ecpg.trailer +++ b/src/interfaces/ecpg/preproc/ecpg.trailer @@ -651,6 +651,34 @@ var_type: simple_type $$.type_index = mm_strdup("-1"); $$.type_sizeof = NULL; } + | STRING_P + { + if (INFORMIX_MODE) + { + /* In Informix mode, "string" is automatically a typedef */ + $$.type_enum = ECPGt_string; + $$.type_str = mm_strdup("char"); + $$.type_dimension = mm_strdup("-1"); + $$.type_index = mm_strdup("-1"); + $$.type_sizeof = NULL; + } + else + { + /* Otherwise, legal only if user typedef'ed it */ + struct typedefs *this = get_typedef("string", false); + + $$.type_str = (this->type->type_enum == ECPGt_varchar || this->type->type_enum == ECPGt_bytea) ? EMPTY : mm_strdup(this->name); + $$.type_enum = this->type->type_enum; + $$.type_dimension = this->type->type_dimension; + $$.type_index = this->type->type_index; + if (this->type->type_sizeof && strlen(this->type->type_sizeof) != 0) + $$.type_sizeof = this->type->type_sizeof; + else + $$.type_sizeof = cat_str(3, mm_strdup("sizeof("), mm_strdup(this->name), mm_strdup(")")); + + struct_member_list[struct_level] = ECPGstruct_member_dup(this->struct_member_list); + } + } | INTERVAL ecpg_interval { $$.type_enum = ECPGt_interval; diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out new file mode 100644 index 0000000000..8b87580752 --- /dev/null +++ b/src/test/regress/expected/json_sqljson.out @@ -0,0 +1,18 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); +ERROR: JSON_EXISTS() is not yet implemented for the json type +LINE 1: SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_VALUE +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); +ERROR: JSON_VALUE() is not yet implemented for the json type +LINE 1: SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_QUERY +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); +ERROR: JSON_QUERY() is not yet implemented for the json type +LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out new file mode 100644 index 0000000000..22f8679917 --- /dev/null +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -0,0 +1,1042 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL::jsonb, '$'); + json_exists +------------- + +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb 'null', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_EXISTS(jsonb 'null', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{}', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + json_exists +------------- + f +(1 row) + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + json_exists +------------- + t +(1 row) + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); + json_exists +------------- + 1 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); + json_exists +------------- + 0 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); + json_exists +------------- + true +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +ERROR: cannot cast type boolean to jsonb +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +ERROR: cannot cast type boolean to real +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_EXISTS() +LINE 1: ...CT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSO... + ^ +-- JSON_VALUE +SELECT JSON_VALUE(NULL::jsonb, '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$'); + json_value +------------ + true +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + json_value +------------ + t +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); + json_value +------------ + 123 +(1 row) + +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); +ERROR: SQL/JSON item cannot be cast to target type +SELECT JSON_VALUE(jsonb '1.23', '$'); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "1.23" +SELECT JSON_VALUE(jsonb '"aaa"', '$'); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); + json_value +------------ + aa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "aaa" +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); + json_value +------------ + 111 +(1 row) + +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + ?column? +------------ + 03-01-2017 +(1 row) + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '[]', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '{}', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '1', '$.a'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); + json_value +------------ + error +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 3 +(1 row) + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); + json_value +------------ + 0 +(1 row) + +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: " " +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 5 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_VALUE() +LINE 1: ...CT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSO... + ^ +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + x | y +---+---- + 0 | -2 + 1 | 2 + 2 | -1 +(3 rows) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); + json_value +------------ + (1,2) +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + json_value +------------ + (1,2) +(1 row) + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); + json_value +-------------------------- + Tue Feb 20 18:34:56 2018 +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- JSON_QUERY +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + json_query | json_query | json_query | json_query | json_query +--------------------+--------------------+--------------------+----------------------+---------------------- + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, null, "2"] | [1, null, "2"] | [1, null, "2"] | [[1, null, "2"]] | [[1, null, "2"]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] +(6 rows) + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + unspec | without | with cond | with uncond | with +--------------------+--------------------+---------------------+----------------------+---------------------- + | | | | + | | | | + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, 2, 3] | [1, 2, 3] | [1, 2, 3] | [[1, 2, 3]] | [[1, 2, 3]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] + | | [1, "2", null, [3]] | [1, "2", null, [3]] | [1, "2", null, [3]] +(9 rows) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + json_query +------------ + \x616161 +(1 row) + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTE... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTE... + ^ +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_QUERY should return singleton item without wrapper +HINT: Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array. +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); + json_query +------------ + [1, +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); +ERROR: expected JSON array +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + x | y | list +---+---+-------------- + 0 | 0 | [] + 0 | 1 | [1] + 0 | 2 | [1, 2] + 0 | 3 | [1, 2, 3] + 0 | 4 | [1, 2, 3, 4] + 1 | 0 | [] + 1 | 1 | [1] + 1 | 2 | [1, 2] + 1 | 3 | [1, 2, 3] + 1 | 4 | [1, 2, 3, 4] + 2 | 0 | [] + 2 | 1 | [] + 2 | 2 | [2] + 2 | 3 | [2, 3] + 2 | 4 | [2, 3, 4] + 3 | 0 | [] + 3 | 1 | [] + 3 | 2 | [] + 3 | 3 | [3] + 3 | 4 | [3, 4] + 4 | 0 | [] + 4 | 1 | [] + 4 | 2 | [] + 4 | 3 | [] + 4 | 4 | [4] +(25 rows) + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); + json_query +----------------------------------------------------- + (1,aaa,"[1, ""2"", {}]","{""x"": [1, ""2"", {}]}",) +(1 row) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); + unnest +------------------------ + {"a": 1, "b": ["foo"]} + {"a": 2, "c": {}} + 123 +(3 rows) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); + json_query +-------------- + {1,2,NULL,3} +(1 row) + +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); + json_query +------------ + 1 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- Test constraints +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); +\d test_jsonb_constraints + Table "public.test_jsonb_constraints" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+-------------------------------------------------------------------------------- + js | text | | | + i | integer | | | + x | jsonb | | | JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +Check constraints: + "test_jsonb_constraint1" CHECK (js IS JSON) + "test_jsonb_constraint2" CHECK (JSON_EXISTS(js::jsonb, '$."a"' PASSING i + 5 AS int, i::text AS txt, ARRAY[1, 2, 3] AS arr)) + "test_jsonb_constraint3" CHECK (JSON_VALUE(js::jsonb, '$."a"' RETURNING integer DEFAULT ('12'::text || i)::integer ON EMPTY ERROR ON ERROR) > i) + "test_jsonb_constraint4" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb) + "test_jsonb_constraint5" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C")) + "test_jsonb_constraint6" CHECK (JSON_EXISTS(js::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2) + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + check_clause +-------------------------------------------------------------------------------------------------------------------------- + ((JSON_EXISTS((js)::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2)) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C"))) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb)) + ((JSON_VALUE((js)::jsonb, '$."a"' RETURNING integer DEFAULT (('12'::text || i))::integer ON EMPTY ERROR ON ERROR) > i)) + ((js IS JSON)) + (JSON_EXISTS((js)::jsonb, '$."a"' PASSING (i + 5) AS int, (i)::text AS txt, ARRAY[1, 2, 3] AS arr)) +(6 rows) + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +WHERE constraint_name LIKE 'test_jsonb_constraint%'; +ERROR: syntax error at or near "WHERE" +LINE 1: WHERE constraint_name LIKE 'test_jsonb_constraint%'; + ^ +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint1" +DETAIL: Failing row contains (, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains (1, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('[]'); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ([], null, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ({"b": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint3" +DETAIL: Failing row contains ({"a": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint5" +DETAIL: Failing row contains ({"a": 7}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint4" +DETAIL: Failing row contains ({"a": 10}, 1, [1, 2]). +DROP TABLE test_jsonb_constraints; +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 4df9d8503b..9b0ecf049d 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -103,7 +103,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo # ---------- # Another group of parallel tests (JSON related) # ---------- -test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson +test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson json_sqljson jsonb_sqljson # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql new file mode 100644 index 0000000000..4f30fa46b9 --- /dev/null +++ b/src/test/regress/sql/json_sqljson.sql @@ -0,0 +1,11 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + +-- JSON_QUERY + +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql new file mode 100644 index 0000000000..b8a6148b7b --- /dev/null +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -0,0 +1,327 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL::jsonb, '$'); + +SELECT JSON_EXISTS(jsonb '[]', '$'); +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + +SELECT JSON_EXISTS(jsonb '1', '$'); +SELECT JSON_EXISTS(jsonb 'null', '$'); +SELECT JSON_EXISTS(jsonb '[]', '$'); + +SELECT JSON_EXISTS(jsonb '1', '$.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_EXISTS(jsonb 'null', '$.a'); +SELECT JSON_EXISTS(jsonb '[]', '$.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); +SELECT JSON_EXISTS(jsonb '{}', '$.a'); +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSON); -- RETURNING FORMAT not allowed + + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL::jsonb, '$'); + +SELECT JSON_VALUE(jsonb 'null', '$'); +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + +SELECT JSON_VALUE(jsonb 'true', '$'); +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + +SELECT JSON_VALUE(jsonb '123', '$'); +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1.23', '$'); +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '"aaa"', '$'); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); + +SELECT JSON_VALUE(jsonb '[]', '$'); +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '{}', '$'); +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1', '$.a'); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed + +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- JSON_QUERY + +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); + +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); + +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); + +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- Test constraints + +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); + +\d test_jsonb_constraints + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; +WHERE constraint_name LIKE 'test_jsonb_constraint%'; + +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +INSERT INTO test_jsonb_constraints VALUES ('[]'); +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); + +DROP TABLE test_jsonb_constraints; + +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index d251fb9a91..6e4c026492 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1254,14 +1254,24 @@ JsonArrayAgg JsonArrayConstructor JsonArrayQueryConstructor JsonBaseObjectInfo +JsonBehavior +JsonBehaviorType +JsonCoercion +JsonCoercionState +JsonCommon JsonConstructorExpr JsonConstructorExprState JsonConstructorType JsonEncoding +JsonExpr +JsonExprOp +JsonExprState JsonFormat JsonFormatType JsonHashEntry JsonIsPredicate +JsonItemCoercions +JsonItemCoercionsState JsonIterateStringValuesAction JsonKeyValue JsonLexContext @@ -1294,6 +1304,10 @@ JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback JsonPathString +JsonPathVarCallback +JsonPathVariable +JsonPathVariableEvalContext +JsonQuotes JsonReturning JsonSemAction JsonSerializeExpr -- 2.35.3
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-18 15:53  Alvaro Herrera <alvherre@alvh.no-ip.org>
  parent: Amit Langote <amitlangote09@gmail.com>
  1 sibling, 1 reply; 300+ messages in thread

From: Alvaro Herrera @ 2023-07-18 15:53 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Erik Rijkers <er@xs4all.nl>; jian he <jian.universality@gmail.com>; pgsql-hackers; Andrew Dunstan <andrew@dunslane.net>

On 2023-Jul-18, Amit Langote wrote:

> Attached updated patches.  In 0002, I removed the mention of the
> RETURNING clause in the JSON(), JSON_SCALAR() documentation, which I
> had forgotten to do in the last version which removed its support in
> code.

> I think 0001 looks ready to go.  Alvaro?

It looks reasonable to me.

> Also, I've been wondering if it isn't too late to apply the following
> to v16 too, so as to make the code look similar in both branches:

Hmm.

> 785480c953 Pass constructName to transformJsonValueExpr()

I think 785480c953 can easily be considered a bugfix on 7081ac46ace8, so
I agree it's better to apply it to 16.

> b6e1157e7d Don't include CaseTestExpr in JsonValueExpr.formatted_expr

I feel a bit uneasy about this one.  It seems to assume that
formatted_expr is always set, but at the same time it's not obvious that
it is.  (Maybe this aspect just needs some more commentary).  I agree
that it would be better to make both branches identical, because if
there's a problem, we are better equipped to get a fix done to both.

As for the removal of makeCaseTestExpr(), I agree -- of the six callers
of makeNode(CastTestExpr), only two of them would be able to use the new
function, so it doesn't look of general enough usefulness.

-- 
Álvaro Herrera        Breisgau, Deutschland  —  https://www.EnterpriseDB.com/
Y una voz del caos me habló y me dijo
"Sonríe y sé feliz, podría ser peor".
Y sonreí. Y fui feliz.
Y fue peor.





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-19 08:17  Amit Langote <amitlangote09@gmail.com>
  parent: Alvaro Herrera <alvherre@alvh.no-ip.org>
  0 siblings, 2 replies; 300+ messages in thread

From: Amit Langote @ 2023-07-19 08:17 UTC (permalink / raw)
  To: Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: Erik Rijkers <er@xs4all.nl>; jian he <jian.universality@gmail.com>; pgsql-hackers; Andrew Dunstan <andrew@dunslane.net>

On Wed, Jul 19, 2023 at 12:53 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
> On 2023-Jul-18, Amit Langote wrote:
>
> > Attached updated patches.  In 0002, I removed the mention of the
> > RETURNING clause in the JSON(), JSON_SCALAR() documentation, which I
> > had forgotten to do in the last version which removed its support in
> > code.
>
> > I think 0001 looks ready to go.  Alvaro?
>
> It looks reasonable to me.

Thanks for taking another look.

I will push this tomorrow.

> > Also, I've been wondering if it isn't too late to apply the following
> > to v16 too, so as to make the code look similar in both branches:
>
> Hmm.
>
> > 785480c953 Pass constructName to transformJsonValueExpr()
>
> I think 785480c953 can easily be considered a bugfix on 7081ac46ace8, so
> I agree it's better to apply it to 16.

OK.

> > b6e1157e7d Don't include CaseTestExpr in JsonValueExpr.formatted_expr
>
> I feel a bit uneasy about this one.  It seems to assume that
> formatted_expr is always set, but at the same time it's not obvious that
> it is.  (Maybe this aspect just needs some more commentary).

Hmm, I agree that the comments about formatted_expr could be improved
further, for which I propose the attached.  Actually, staring some
more at this, I'm inclined to change makeJsonValueExpr() to allow
callers to pass it the finished 'formatted_expr' rather than set it by
themselves.

>  I agree
> that it would be better to make both branches identical, because if
> there's a problem, we are better equipped to get a fix done to both.
>
> As for the removal of makeCaseTestExpr(), I agree -- of the six callers
> of makeNode(CastTestExpr), only two of them would be able to use the new
> function, so it doesn't look of general enough usefulness.

OK, so you agree with back-patching this one too, though perhaps only
after applying something like the aforementioned patch.  Just to be
sure, would the good practice in this case be to squash the fixup
patch into b6e1157e7d before back-patching?


--
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

Attachments:

0001-Code-review-for-commit-b6e1157e7d.patch (application/octet-stream, 5.4K) download From 05457853ca39eccabb310342edbaeb232c50de6d Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 19 Jul 2023 16:04:36 +0900 Subject: [PATCH] Code review for commit b6e1157e7d MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit b6e1157e7d made some changes to enforce that JsonValueExpr.formatted_expr is always set and is the expression that gives a JsonValueExpr its runtime value, but that's not really apparent from the comments about and the code manipulating formatted_expr. This commit fixes that. Per suggestion from Álvaro Herrera. Discussion: https://postgr.es/m/20230718155313.3wqg6encgt32adqb%40alvherre.pgsql --- src/backend/nodes/makefuncs.c | 11 ++++------- src/backend/nodes/nodeFuncs.c | 4 +--- src/backend/parser/gram.y | 4 +++- src/backend/parser/parse_expr.c | 9 +++------ src/include/nodes/makefuncs.h | 3 ++- src/include/nodes/primnodes.h | 5 +++-- 6 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index c6c310d253..0e7e6e46d9 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -848,16 +848,13 @@ makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location) * creates a JsonValueExpr node */ JsonValueExpr * -makeJsonValueExpr(Expr *expr, JsonFormat *format) +makeJsonValueExpr(Expr *raw_expr, Expr *formatted_expr, + JsonFormat *format) { JsonValueExpr *jve = makeNode(JsonValueExpr); - jve->raw_expr = expr; - - /* - * Set after checking the format, if needed, in transformJsonValueExpr(). - */ - jve->formatted_expr = NULL; + jve->raw_expr = raw_expr; + jve->formatted_expr = formatted_expr; jve->format = format; return jve; diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index c41e6bb984..503d76aae0 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -225,9 +225,7 @@ exprType(const Node *expr) { const JsonValueExpr *jve = (const JsonValueExpr *) expr; - type = exprType((Node *) - (jve->formatted_expr ? jve->formatted_expr : - jve->raw_expr)); + type = exprType((Node *) jve->formatted_expr); } break; case T_JsonConstructorExpr: diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index edb6c00ece..c31b373358 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -16353,7 +16353,9 @@ opt_asymmetric: ASYMMETRIC json_value_expr: a_expr json_format_clause_opt { - $$ = (Node *) makeJsonValueExpr((Expr *) $1, castNode(JsonFormat, $2)); + /* formatted_expr will be set during parse-analysis. */ + $$ = (Node *) makeJsonValueExpr((Expr *) $1, NULL, + castNode(JsonFormat, $2)); } ; diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 5a05caa874..03097ee9c3 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -3631,13 +3631,12 @@ transformJsonArrayQueryConstructor(ParseState *pstate, makeString(pstrdup("a"))); colref->location = ctor->location; - agg->arg = makeJsonValueExpr((Expr *) colref, ctor->format); - /* * No formatting necessary, so set formatted_expr to be the same as * raw_expr. */ - agg->arg->formatted_expr = agg->arg->raw_expr; + agg->arg = makeJsonValueExpr((Expr *) colref, (Expr *) colref, + ctor->format); agg->absent_on_null = ctor->absent_on_null; agg->constructor = makeNode(JsonAggConstructor); agg->constructor->agg_order = NIL; @@ -3906,9 +3905,7 @@ transformJsonParseArg(ParseState *pstate, Node *jsexpr, JsonFormat *format, expr = makeJsonByteaToTextConversion(expr, format, exprLocation(expr)); *exprtype = TEXTOID; - jve = makeJsonValueExpr((Expr *) raw_expr, format); - - jve->formatted_expr = (Expr *) expr; + jve = makeJsonValueExpr((Expr *) raw_expr, expr, format); expr = (Node *) jve; } else diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 06d991b725..3180703005 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -110,7 +110,8 @@ extern VacuumRelation *makeVacuumRelation(RangeVar *relation, Oid oid, List *va_ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); -extern JsonValueExpr *makeJsonValueExpr(Expr *expr, JsonFormat *format); +extern JsonValueExpr *makeJsonValueExpr(Expr *raw_expr, Expr *formatted_expr, + JsonFormat *format); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 0d2df069b3..e1aadc39cf 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1596,8 +1596,9 @@ typedef struct JsonReturning * JsonValueExpr - * representation of JSON value expression (expr [FORMAT JsonFormat]) * - * Note that raw_expr is only there for displaying and is not evaluated by - * ExecInterpExpr() and eval_const_exprs_mutator(). + * The actual value is obtained by evaluating formatted_expr. raw_expr is + * only there for displaying the original user-written expression and is not + * evaluated by ExecInterpExpr() and eval_const_exprs_mutator(). */ typedef struct JsonValueExpr { -- 2.35.3
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-19 12:46  Amit Langote <amitlangote09@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  1 sibling, 0 replies; 300+ messages in thread

From: Amit Langote @ 2023-07-19 12:46 UTC (permalink / raw)
  To: Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: Erik Rijkers <er@xs4all.nl>; jian he <jian.universality@gmail.com>; pgsql-hackers; Andrew Dunstan <andrew@dunslane.net>

On Wed, Jul 19, 2023 at 5:17 PM Amit Langote <amitlangote09@gmail.com> wrote:
> On Wed, Jul 19, 2023 at 12:53 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
> > On 2023-Jul-18, Amit Langote wrote:
> > > b6e1157e7d Don't include CaseTestExpr in JsonValueExpr.formatted_expr
> >
> > I feel a bit uneasy about this one.  It seems to assume that
> > formatted_expr is always set, but at the same time it's not obvious that
> > it is.  (Maybe this aspect just needs some more commentary).
>
> Hmm, I agree that the comments about formatted_expr could be improved
> further, for which I propose the attached.  Actually, staring some
> more at this, I'm inclined to change makeJsonValueExpr() to allow
> callers to pass it the finished 'formatted_expr' rather than set it by
> themselves.

Hmm, after looking some more, it may not be entirely right that
formatted_expr is always set in the code paths that call
transformJsonValueExpr().  Will look at this some more tomorrow.

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-20 01:35  jian he <jian.universality@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  1 sibling, 1 reply; 300+ messages in thread

From: jian he @ 2023-07-20 01:35 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Erik Rijkers <er@xs4all.nl>; Alvaro Herrera <alvherre@alvh.no-ip.org>; pgsql-hackers; Andrew Dunstan <andrew@dunslane.net>

On Tue, Jul 18, 2023 at 5:11 PM Amit Langote <amitlangote09@gmail.com>
wrote:
>
> Hi,
>
> On Mon, Jul 17, 2023 at 4:14 PM Erik Rijkers <er@xs4all.nl> wrote:
> > Op 7/17/23 om 07:00 schreef jian he:
> > > hi.
> > > seems there is no explanation about, json_api_common_syntax in
> > > functions-json.html
> > >
> > > I can get json_query full synopsis from functions-json.html as
follows:
> > > json_query ( context_item, path_expression [ PASSING { value AS
> > > varname } [, ...]] [ RETURNING data_type [ FORMAT JSON [ ENCODING UTF8
> > > ] ] ] [ { WITHOUT | WITH { CONDITIONAL | [UNCONDITIONAL] } } [ ARRAY ]
> > > WRAPPER ] [ { KEEP | OMIT } QUOTES [ ON SCALAR STRING ] ] [ { ERROR |
> > > NULL | EMPTY { [ ARRAY ] | OBJECT } | DEFAULT expression } ON EMPTY ]
> > > [ { ERROR | NULL | EMPTY { [ ARRAY ] | OBJECT } | DEFAULT expression }
> > > ON ERROR ])
> > >
> > > seems doesn't  have a full synopsis for json_table? only partial one
> > > by  one explanation.
>
> I looked through the history of the docs portion of the patch and it
> looks like the synopsis for JSON_TABLE(...) used to be there but was
> taken out during one of the doc reworks [1].
>
> I've added it back in the patch as I agree that it would help to have
> it.   Though, I am not totally sure where I've put it is the right
> place for it.  JSON_TABLE() is a beast that won't fit into the table
> that JSON_QUERY() et al are in, so maybe that's how it will have to
> be?  I have no better idea.
>
> >

attached screenshot render json_table syntax almost plain html. It looks
fine.
based on syntax, then I am kind of confused with following 2 cases:
--1
SELECT * FROM JSON_TABLE(jsonb '1', '$'
    COLUMNS (a int PATH 'strict $.a' default 1  ON EMPTY default 2 on
error)
    ERROR ON ERROR) jt;

--2
SELECT * FROM JSON_TABLE(jsonb '1', '$'
    COLUMNS (a int PATH 'strict $.a' default 1  ON EMPTY default 2 on
error)) jt;

the first one should yield syntax error?
[image: json_table_v8.png]

Attachments:

json_table_v8.png (image/png, 416.6K) download view image
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-20 07:03  Amit Langote <amitlangote09@gmail.com>
  parent: jian he <jian.universality@gmail.com>
  0 siblings, 0 replies; 300+ messages in thread

From: Amit Langote @ 2023-07-20 07:03 UTC (permalink / raw)
  To: jian he <jian.universality@gmail.com>; +Cc: Erik Rijkers <er@xs4all.nl>; Alvaro Herrera <alvherre@alvh.no-ip.org>; pgsql-hackers; Andrew Dunstan <andrew@dunslane.net>

Hello,

On Thu, Jul 20, 2023 at 10:35 AM jian he <jian.universality@gmail.com> wrote:
> On Tue, Jul 18, 2023 at 5:11 PM Amit Langote <amitlangote09@gmail.com> wrote:
> > > Op 7/17/23 om 07:00 schreef jian he:
> > > > hi.
> > > > seems there is no explanation about, json_api_common_syntax in
> > > > functions-json.html
> > > >
> > > > I can get json_query full synopsis from functions-json.html as follows:
> > > > json_query ( context_item, path_expression [ PASSING { value AS
> > > > varname } [, ...]] [ RETURNING data_type [ FORMAT JSON [ ENCODING UTF8
> > > > ] ] ] [ { WITHOUT | WITH { CONDITIONAL | [UNCONDITIONAL] } } [ ARRAY ]
> > > > WRAPPER ] [ { KEEP | OMIT } QUOTES [ ON SCALAR STRING ] ] [ { ERROR |
> > > > NULL | EMPTY { [ ARRAY ] | OBJECT } | DEFAULT expression } ON EMPTY ]
> > > > [ { ERROR | NULL | EMPTY { [ ARRAY ] | OBJECT } | DEFAULT expression }
> > > > ON ERROR ])
> > > >
> > > > seems doesn't  have a full synopsis for json_table? only partial one
> > > > by  one explanation.
> >
> > I looked through the history of the docs portion of the patch and it
> > looks like the synopsis for JSON_TABLE(...) used to be there but was
> > taken out during one of the doc reworks [1].
> >
> > I've added it back in the patch as I agree that it would help to have
> > it.   Though, I am not totally sure where I've put it is the right
> > place for it.  JSON_TABLE() is a beast that won't fit into the table
> > that JSON_QUERY() et al are in, so maybe that's how it will have to
> > be?  I have no better idea.
>
> attached screenshot render json_table syntax almost plain html. It looks fine.

Thanks for checking.

> based on syntax, then I am kind of confused with following 2 cases:
> --1
> SELECT * FROM JSON_TABLE(jsonb '1', '$'
>     COLUMNS (a int PATH 'strict $.a' default 1  ON EMPTY default 2 on error)
>     ERROR ON ERROR) jt;
>
> --2
> SELECT * FROM JSON_TABLE(jsonb '1', '$'
>     COLUMNS (a int PATH 'strict $.a' default 1  ON EMPTY default 2 on error)) jt;
>
> the first one should yield syntax error?

No.  Actually, the synopsis missed the optional ON ERROR clause that
can appear after COLUMNS(...).  Will fix it.

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-20 08:19  Amit Langote <amitlangote09@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  1 sibling, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-07-20 08:19 UTC (permalink / raw)
  To: Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: Erik Rijkers <er@xs4all.nl>; jian he <jian.universality@gmail.com>; pgsql-hackers; Andrew Dunstan <andrew@dunslane.net>

On Wed, Jul 19, 2023 at 5:17 PM Amit Langote <amitlangote09@gmail.com> wrote:
> On Wed, Jul 19, 2023 at 12:53 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
> > On 2023-Jul-18, Amit Langote wrote:
> >
> > > Attached updated patches.  In 0002, I removed the mention of the
> > > RETURNING clause in the JSON(), JSON_SCALAR() documentation, which I
> > > had forgotten to do in the last version which removed its support in
> > > code.
> >
> > > I think 0001 looks ready to go.  Alvaro?
> >
> > It looks reasonable to me.
>
> Thanks for taking another look.
>
> I will push this tomorrow.

Pushed.

> > > Also, I've been wondering if it isn't too late to apply the following
> > > to v16 too, so as to make the code look similar in both branches:
> >
> > Hmm.
> >
> > > 785480c953 Pass constructName to transformJsonValueExpr()
> >
> > I think 785480c953 can easily be considered a bugfix on 7081ac46ace8, so
> > I agree it's better to apply it to 16.
>
> OK.

Pushed to 16.

> > > b6e1157e7d Don't include CaseTestExpr in JsonValueExpr.formatted_expr
> >
> > I feel a bit uneasy about this one.  It seems to assume that
> > formatted_expr is always set, but at the same time it's not obvious that
> > it is.  (Maybe this aspect just needs some more commentary).
>
> Hmm, I agree that the comments about formatted_expr could be improved
> further, for which I propose the attached.  Actually, staring some
> more at this, I'm inclined to change makeJsonValueExpr() to allow
> callers to pass it the finished 'formatted_expr' rather than set it by
> themselves.
>
> >  I agree
> > that it would be better to make both branches identical, because if
> > there's a problem, we are better equipped to get a fix done to both.
> >
> > As for the removal of makeCaseTestExpr(), I agree -- of the six callers
> > of makeNode(CastTestExpr), only two of them would be able to use the new
> > function, so it doesn't look of general enough usefulness.
>
> OK, so you agree with back-patching this one too, though perhaps only
> after applying something like the aforementioned patch.

I looked at this some more and concluded that it's fine to think that
all JsonValueExpr nodes leaving the parser have their formatted_expr
set.  I've updated the commentary some more in the patch attached as
0001.

Rebased SQL/JSON patches also attached.  I've fixed the JSON_TABLE
syntax synopsis in the documentation as mentioned in my other email.

--
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

Attachments: download all (5 files, .tar.gz)

v9-0001-Code-review-for-commit-b6e1157e7d.patch (application/octet-stream, 6.1K) download From fb62a4c159d61ef1595cde9796c11640a32040a0 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Wed, 19 Jul 2023 16:04:36 +0900 Subject: [PATCH v9 1/5] Code review for commit b6e1157e7d MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit b6e1157e7d made some changes to enforce that JsonValueExpr.formatted_expr is always set and is the expression that gives a JsonValueExpr its runtime value, but that's not really apparent from the comments about and the code manipulating formatted_expr. This commit fixes that. Per suggestion from Álvaro Herrera. Discussion: https://postgr.es/m/20230718155313.3wqg6encgt32adqb%40alvherre.pgsql --- src/backend/nodes/makefuncs.c | 11 ++++------- src/backend/nodes/nodeFuncs.c | 4 +--- src/backend/parser/gram.y | 4 +++- src/backend/parser/parse_expr.c | 17 +++++++++++------ src/include/nodes/makefuncs.h | 3 ++- src/include/nodes/primnodes.h | 5 +++-- 6 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index c6c310d253..0e7e6e46d9 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -848,16 +848,13 @@ makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location) * creates a JsonValueExpr node */ JsonValueExpr * -makeJsonValueExpr(Expr *expr, JsonFormat *format) +makeJsonValueExpr(Expr *raw_expr, Expr *formatted_expr, + JsonFormat *format) { JsonValueExpr *jve = makeNode(JsonValueExpr); - jve->raw_expr = expr; - - /* - * Set after checking the format, if needed, in transformJsonValueExpr(). - */ - jve->formatted_expr = NULL; + jve->raw_expr = raw_expr; + jve->formatted_expr = formatted_expr; jve->format = format; return jve; diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index c41e6bb984..503d76aae0 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -225,9 +225,7 @@ exprType(const Node *expr) { const JsonValueExpr *jve = (const JsonValueExpr *) expr; - type = exprType((Node *) - (jve->formatted_expr ? jve->formatted_expr : - jve->raw_expr)); + type = exprType((Node *) jve->formatted_expr); } break; case T_JsonConstructorExpr: diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 7a44a374e4..60080e877e 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -16362,7 +16362,9 @@ opt_asymmetric: ASYMMETRIC json_value_expr: a_expr json_format_clause_opt { - $$ = (Node *) makeJsonValueExpr((Expr *) $1, castNode(JsonFormat, $2)); + /* formatted_expr will be set during parse-analysis. */ + $$ = (Node *) makeJsonValueExpr((Expr *) $1, NULL, + castNode(JsonFormat, $2)); } ; diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 5a05caa874..c08c06373a 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -3205,6 +3205,10 @@ makeJsonByteaToTextConversion(Node *expr, JsonFormat *format, int location) /* * Transform JSON value expression using specified input JSON format or * default format otherwise. + * + * Returned expression is either ve->raw_expr coerced to text (if needed) or + * a JsonValueExpr with formatted_expr set to the coerced copy of raw_expr + * if the specified format requires it. */ static Node * transformJsonValueExpr(ParseState *pstate, const char *constructName, @@ -3304,6 +3308,10 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, } } + /* If returning a JsonValueExpr, formatted_expr must have been set. */ + Assert(!IsA(expr, JsonValueExpr) || + ((JsonValueExpr *) expr)->formatted_expr != NULL); + return expr; } @@ -3631,13 +3639,12 @@ transformJsonArrayQueryConstructor(ParseState *pstate, makeString(pstrdup("a"))); colref->location = ctor->location; - agg->arg = makeJsonValueExpr((Expr *) colref, ctor->format); - /* * No formatting necessary, so set formatted_expr to be the same as * raw_expr. */ - agg->arg->formatted_expr = agg->arg->raw_expr; + agg->arg = makeJsonValueExpr((Expr *) colref, (Expr *) colref, + ctor->format); agg->absent_on_null = ctor->absent_on_null; agg->constructor = makeNode(JsonAggConstructor); agg->constructor->agg_order = NIL; @@ -3906,9 +3913,7 @@ transformJsonParseArg(ParseState *pstate, Node *jsexpr, JsonFormat *format, expr = makeJsonByteaToTextConversion(expr, format, exprLocation(expr)); *exprtype = TEXTOID; - jve = makeJsonValueExpr((Expr *) raw_expr, format); - - jve->formatted_expr = (Expr *) expr; + jve = makeJsonValueExpr((Expr *) raw_expr, (Expr *) expr, format); expr = (Node *) jve; } else diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 06d991b725..3180703005 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -110,7 +110,8 @@ extern VacuumRelation *makeVacuumRelation(RangeVar *relation, Oid oid, List *va_ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); -extern JsonValueExpr *makeJsonValueExpr(Expr *expr, JsonFormat *format); +extern JsonValueExpr *makeJsonValueExpr(Expr *raw_expr, Expr *formatted_expr, + JsonFormat *format); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 0d2df069b3..e1aadc39cf 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1596,8 +1596,9 @@ typedef struct JsonReturning * JsonValueExpr - * representation of JSON value expression (expr [FORMAT JsonFormat]) * - * Note that raw_expr is only there for displaying and is not evaluated by - * ExecInterpExpr() and eval_const_exprs_mutator(). + * The actual value is obtained by evaluating formatted_expr. raw_expr is + * only there for displaying the original user-written expression and is not + * evaluated by ExecInterpExpr() and eval_const_exprs_mutator(). */ typedef struct JsonValueExpr { -- 2.35.3
v9-0002-SQL-JSON-functions.patch (application/octet-stream, 50.5K) download From e79ccc92a577d67398eca6ba96c0c02b514f7ed3 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Tue, 18 Jul 2023 17:58:28 +0900 Subject: [PATCH v9 2/5] SQL JSON functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This Patch introduces three SQL standard JSON functions: JSON() JSON_SCALAR() JSON_SERIALIZE() JSON() produces json values from text, bytea, json or jsonb values, and has facilitites for handling duplicate keys. JSON_SCALAR() produces a json value from any scalar sql value, including json and jsonb. JSON_SERIALIZE() produces text or bytea from input which containis or represents json or jsonb; For the most part these functions don't add any significant new capabilities, but they will be of use to users wanting standard compliant JSON handling. Author: Nikita Glukhov <n.gluhov@postgrespro.ru> Author: Teodor Sigaev <teodor@sigaev.ru> Author: Oleg Bartunov <obartunov@gmail.com> Author: Alexander Korotkov <aekorotkov@gmail.com> Author: Andrew Dunstan <andrew@dunslane.net> Author: Amit Langote <amitlangote09@gmail.com> Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- doc/src/sgml/func.sgml | 65 +++++ src/backend/executor/execExpr.c | 30 +++ src/backend/executor/execExprInterp.c | 43 +++- src/backend/nodes/nodeFuncs.c | 30 +++ src/backend/parser/gram.y | 68 +++++- src/backend/parser/parse_expr.c | 208 ++++++++++++++-- src/backend/parser/parse_target.c | 9 + src/backend/utils/adt/format_type.c | 4 + src/backend/utils/adt/json.c | 21 +- src/backend/utils/adt/jsonb.c | 48 +++- src/backend/utils/adt/ruleutils.c | 14 +- src/include/nodes/parsenodes.h | 48 ++++ src/include/nodes/primnodes.h | 5 +- src/include/parser/kwlist.h | 4 +- src/include/utils/jsonb.h | 2 +- src/include/utils/jsonfuncs.h | 4 + src/test/regress/expected/sqljson.out | 332 ++++++++++++++++++++++++++ src/test/regress/sql/sqljson.sql | 85 +++++++ src/tools/pgindent/typedefs.list | 1 + 19 files changed, 972 insertions(+), 49 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index b94827674c..b8102eee22 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16001,6 +16001,71 @@ table2-mapping <returnvalue>{"a": "1", "b": "2"}</returnvalue> </para></entry> </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json constructor</primary></indexterm> + <function>json</function> ( + <replaceable>expression</replaceable> + <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional></optional> + <optional> { <literal>WITH</literal> | <literal>WITHOUT</literal> } <literal>UNIQUE</literal> <optional> <literal>KEYS</literal> </optional></optional> + </para> + <para> + The <replaceable>expression</replaceable> can be any text type or a + <type>bytea</type> in UTF8 encoding. If the + <replaceable>expression</replaceable> is NULL, an + <acronym>SQL</acronym> null value is returned. + If <literal>WITH UNIQUE</literal> is specified, the + <replaceable>expression</replaceable> must not contain any duplicate + object keys. + </para> + <para> + <literal>json('{"a":123, "b":[true,"foo"], "a":"bar"}')</literal> + <returnvalue>{"a":123, "b":[true,"foo"], "a":"bar"}</returnvalue> + </para> + </entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json_scalar</primary></indexterm> + <function>json_scalar</function> (<replaceable>expression</replaceable>) + </para> + <para> + Returns a JSON scalar value representing + <replaceable>expression</replaceable>. + If the input is NULL, an SQL NULL is returned. If the input is a number + or a boolean value, a corresponding JSON number or boolean value is + returned. For any other value a JSON string is returned. + </para> + <para> + <literal>json_scalar(123.45)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_scalar(CURRENT_TIMESTAMP)</literal> + <returnvalue>"2022-05-10T10:51:04.62128-04:00"</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <function>json_serialize</function> ( + <replaceable>expression</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional>) + </para> + <para> + Transforms an SQL/JSON value into a character or binary string. The + <replaceable>expression</replaceable> can be of any JSON type, any + character string type, or <type>bytea</type> in UTF8 encoding. + The returned type can be any character string type or + <type>bytea</type>. The default is <type>text</type>. + </para> + <para> + <literal>json_serialize('{ "a" : 1 } ' RETURNING bytea)</literal> + <returnvalue>\x7b20226122203a2031207d20</returnvalue> + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index bf3a08c5f0..6ca4098bef 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -48,6 +48,7 @@ #include "utils/array.h" #include "utils/builtins.h" #include "utils/datum.h" +#include "utils/jsonfuncs.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -2311,6 +2312,12 @@ ExecInitExprRec(Expr *node, ExprState *state, { ExecInitExprRec(ctor->func, state, resv, resnull); } + else if ((ctor->type == JSCTOR_JSON_PARSE && !ctor->unique) || + ctor->type == JSCTOR_JSON_SERIALIZE) + { + /* Use the value of the first argument as a result */ + ExecInitExprRec(linitial(args), state, resv, resnull); + } else { JsonConstructorExprState *jcstate; @@ -2349,6 +2356,29 @@ ExecInitExprRec(Expr *node, ExprState *state, argno++; } + /* prepare type cache for datum_to_json[b]() */ + if (ctor->type == JSCTOR_JSON_SCALAR) + { + bool is_jsonb = + ctor->returning->format->format_type == JS_FORMAT_JSONB; + + jcstate->arg_type_cache = + palloc(sizeof(*jcstate->arg_type_cache) * nargs); + + for (int i = 0; i < nargs; i++) + { + JsonTypeCategory category; + Oid outfuncid; + Oid typid = jcstate->arg_types[i]; + + json_categorize_type(typid, is_jsonb, + &category, &outfuncid); + + jcstate->arg_type_cache[i].outfuncid = outfuncid; + jcstate->arg_type_cache[i].category = (int) category; + } + } + ExprEvalPushStep(state, &scratch); } diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 851946a927..76e59691e5 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -3992,7 +3992,7 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_values, jcstate->arg_nulls, jcstate->arg_types, - jcstate->constructor->absent_on_null); + ctor->absent_on_null); else if (ctor->type == JSCTOR_JSON_OBJECT) res = (is_jsonb ? jsonb_build_object_worker : @@ -4002,6 +4002,47 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_types, jcstate->constructor->absent_on_null, jcstate->constructor->unique); + else if (ctor->type == JSCTOR_JSON_SCALAR) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + Oid outfuncid = jcstate->arg_type_cache[0].outfuncid; + JsonTypeCategory category = (JsonTypeCategory) + jcstate->arg_type_cache[0].category; + + if (is_jsonb) + res = to_jsonb_worker(value, category, outfuncid); + else + res = to_json_worker(value, category, outfuncid); + } + } + else if (ctor->type == JSCTOR_JSON_PARSE) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + text *js = DatumGetTextP(value); + + if (is_jsonb) + res = jsonb_from_text(js, true); + else + { + (void) json_validate(js, true, true); + res = value; + } + } + } else elog(ERROR, "invalid JsonConstructorExpr type %d", ctor->type); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 503d76aae0..c03f4f23e2 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -3899,6 +3899,36 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonParseExpr: + { + JsonParseExpr *jpe = (JsonParseExpr *) node; + + if (WALK(jpe->expr)) + return true; + if (WALK(jpe->output)) + return true; + } + break; + case T_JsonScalarExpr: + { + JsonScalarExpr *jse = (JsonScalarExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; + case T_JsonSerializeExpr: + { + JsonSerializeExpr *jse = (JsonSerializeExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; case T_JsonConstructorExpr: { JsonConstructorExpr *ctor = (JsonConstructorExpr *) node; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 60080e877e..99e05d8548 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -566,7 +566,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <list> copy_options %type <typnam> Typename SimpleTypename ConstTypename - GenericType Numeric opt_float + GenericType Numeric opt_float JsonType Character ConstCharacter CharacterWithLength CharacterWithoutLength ConstDatetime ConstInterval @@ -647,7 +647,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> json_format_clause_opt json_value_expr - json_output_clause_opt + json_returning_clause_opt json_name_and_value json_aggregate_func %type <list> json_name_and_value_list @@ -659,7 +659,6 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt - /* * Non-keyword token types. These are hard-wired into the "flex" lexer. * They must be listed first so that their numeric codes do not depend on @@ -723,6 +722,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG + JSON_SCALAR JSON_SERIALIZE KEY KEYS @@ -13990,6 +13990,7 @@ SimpleTypename: $$->typmods = list_make2(makeIntConst(INTERVAL_FULL_RANGE, -1), makeIntConst($3, @3)); } + | JsonType { $$ = $1; } ; /* We have a separate ConstTypename to allow defaulting fixed-length @@ -14008,6 +14009,7 @@ ConstTypename: | ConstBit { $$ = $1; } | ConstCharacter { $$ = $1; } | ConstDatetime { $$ = $1; } + | JsonType { $$ = $1; } ; /* @@ -14376,6 +14378,13 @@ interval_second: } ; +JsonType: + JSON + { + $$ = SystemTypeName("json"); + $$->location = @1; + } + ; /***************************************************************************** * @@ -15570,7 +15579,7 @@ func_expr_common_subexpr: | JSON_OBJECT '(' json_name_and_value_list json_object_constructor_null_clause_opt json_key_uniqueness_constraint_opt - json_output_clause_opt ')' + json_returning_clause_opt ')' { JsonObjectConstructor *n = makeNode(JsonObjectConstructor); @@ -15581,7 +15590,7 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } - | JSON_OBJECT '(' json_output_clause_opt ')' + | JSON_OBJECT '(' json_returning_clause_opt ')' { JsonObjectConstructor *n = makeNode(JsonObjectConstructor); @@ -15595,7 +15604,7 @@ func_expr_common_subexpr: | JSON_ARRAY '(' json_value_expr_list json_array_constructor_null_clause_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayConstructor *n = makeNode(JsonArrayConstructor); @@ -15610,7 +15619,7 @@ func_expr_common_subexpr: select_no_parens json_format_clause_opt /* json_array_constructor_null_clause_opt */ - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayQueryConstructor *n = makeNode(JsonArrayQueryConstructor); @@ -15623,7 +15632,7 @@ func_expr_common_subexpr: $$ = (Node *) n; } | JSON_ARRAY '(' - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayConstructor *n = makeNode(JsonArrayConstructor); @@ -15634,7 +15643,36 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } - ; + | JSON '(' json_value_expr json_key_uniqueness_constraint_opt ')' + { + JsonParseExpr *n = makeNode(JsonParseExpr); + + n->expr = (JsonValueExpr *) $3; + n->unique_keys = $4; + n->output = NULL; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SCALAR '(' a_expr ')' + { + JsonScalarExpr *n = makeNode(JsonScalarExpr); + + n->expr = (Expr *) $3; + n->output = NULL; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SERIALIZE '(' json_value_expr json_returning_clause_opt ')' + { + JsonSerializeExpr *n = makeNode(JsonSerializeExpr); + + n->expr = (JsonValueExpr *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + ; + /* * SQL/XML support @@ -16384,7 +16422,7 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; -json_output_clause_opt: +json_returning_clause_opt: RETURNING Typename json_format_clause_opt { JsonOutput *n = makeNode(JsonOutput); @@ -16457,7 +16495,7 @@ json_aggregate_func: json_name_and_value json_object_constructor_null_clause_opt json_key_uniqueness_constraint_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonObjectAgg *n = makeNode(JsonObjectAgg); @@ -16475,7 +16513,7 @@ json_aggregate_func: json_value_expr json_array_aggregate_order_by_clause_opt json_array_constructor_null_clause_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayAgg *n = makeNode(JsonArrayAgg); @@ -17075,7 +17113,6 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION - | JSON | KEY | KEYS | LABEL @@ -17290,10 +17327,13 @@ col_name_keyword: | INT_P | INTEGER | INTERVAL + | JSON | JSON_ARRAY | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | LEAST | NATIONAL | NCHAR @@ -17654,6 +17694,8 @@ bare_label_keyword: | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | KEY | KEYS | LABEL diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index c08c06373a..e7f988dbd2 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -86,6 +86,10 @@ static Node *transformJsonArrayQueryConstructor(ParseState *pstate, static Node *transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg); static Node *transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg); static Node *transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred); +static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); +static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); +static Node *transformJsonSerializeExpr(ParseState *pstate, + JsonSerializeExpr * expr); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -337,6 +341,18 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonIsPredicate(pstate, (JsonIsPredicate *) expr); break; + case T_JsonParseExpr: + result = transformJsonParseExpr(pstate, (JsonParseExpr *) expr); + break; + + case T_JsonScalarExpr: + result = transformJsonScalarExpr(pstate, (JsonScalarExpr *) expr); + break; + + case T_JsonSerializeExpr: + result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3212,7 +3228,8 @@ makeJsonByteaToTextConversion(Node *expr, JsonFormat *format, int location) */ static Node * transformJsonValueExpr(ParseState *pstate, const char *constructName, - JsonValueExpr *ve, JsonFormatType default_format) + JsonValueExpr *ve, JsonFormatType default_format, + Oid targettype) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3254,12 +3271,14 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, else format = default_format; - if (format != JS_FORMAT_DEFAULT) + if (format != JS_FORMAT_DEFAULT || + (OidIsValid(targettype) && exprtype != targettype)) { - Oid targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; Node *coerced; + bool cast_is_needed = OidIsValid(targettype); - if (exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) + if (!cast_is_needed && + exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), errmsg(ve->format->format_type == JS_FORMAT_DEFAULT ? @@ -3275,6 +3294,9 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, exprtype = TEXTOID; } + if (!OidIsValid(targettype)) + targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; + /* Try to coerce to the target type. */ coerced = coerce_to_target_type(pstate, expr, exprtype, targettype, -1, @@ -3285,11 +3307,20 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, if (!coerced) { /* If coercion failed, use to_json()/to_jsonb() functions. */ - Oid fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; - FuncExpr *fexpr = makeFuncExpr(fnoid, targettype, - list_make1(expr), - InvalidOid, InvalidOid, - COERCE_EXPLICIT_CALL); + FuncExpr *fexpr; + Oid fnoid; + + if (cast_is_needed) /* only CAST is allowed */ + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(exprtype), + format_type_be(targettype)), + parser_errposition(pstate, location))); + + fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; + fexpr = makeFuncExpr(fnoid, targettype, list_make1(expr), + InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL); fexpr->location = location; @@ -3590,7 +3621,8 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) Node *key = transformExprRecurse(pstate, (Node *) kv->key); Node *val = transformJsonValueExpr(pstate, "JSON_OBJECT()", kv->value, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, key); args = lappend(args, val); @@ -3774,9 +3806,8 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) Oid aggtype; key = transformExprRecurse(pstate, (Node *) agg->arg->key); - val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", - agg->arg->value, - JS_FORMAT_DEFAULT); + val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", agg->arg->value, + JS_FORMAT_DEFAULT, InvalidOid); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3834,7 +3865,7 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", agg->arg, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, InvalidOid); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3882,7 +3913,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, "JSON_ARRAY()", jsval, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, val); } @@ -3963,3 +3995,149 @@ transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred) return makeJsonIsPredicate(expr, NULL, pred->item_type, pred->unique_keys, pred->location); } + +/* + * Transform the output clause of a JSON_*() expression if there is one and + * create one if not. + */ +static JsonReturning * +transformJsonReturning(ParseState *pstate, JsonOutput *output, const char *fname) +{ + JsonReturning *returning; + + if (output) + { + returning = transformJsonOutput(pstate, output, false); + + Assert(OidIsValid(returning->typid)); + + if (returning->typid != JSONOID && returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), fname), + parser_errposition(pstate, output->typeName->location))); + } + else + { + /* Output type is JSON by default. */ + Oid targettype = JSONOID; + JsonFormatType format = JS_FORMAT_JSON; + + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(format, JS_ENC_DEFAULT, -1); + returning->typid = targettype; + returning->typmod = -1; + } + + return returning; +} + +/* + * Transform a JSON() expression. + */ +static Node * +transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) +{ + JsonOutput *output = jsexpr->output; + JsonReturning *returning; + Node *arg; + + returning = transformJsonReturning(pstate, output, "JSON()"); + + if (jsexpr->unique_keys) + { + /* + * Coerce string argument to text and then to json[b] in the executor + * node with key uniqueness check. + */ + JsonValueExpr *jve = jsexpr->expr; + Oid arg_type; + + arg = transformJsonParseArg(pstate, (Node *) jve->raw_expr, jve->format, + &arg_type); + + if (arg_type != TEXTOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use non-string types with WITH UNIQUE KEYS clause"), + parser_errposition(pstate, jsexpr->location))); + } + else + { + /* + * Coerce argument to target type using CAST for compatibility with PG + * function-like CASTs. + */ + arg = transformJsonValueExpr(pstate, "JSON()", jsexpr->expr, + JS_FORMAT_JSON, returning->typid); + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, + returning, jsexpr->unique_keys, false, + jsexpr->location); +} + +/* + * Transform a JSON_SCALAR() expression. + */ +static Node * +transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *jsexpr) +{ + Node *arg = transformExprRecurse(pstate, (Node *) jsexpr->expr); + JsonOutput *output = jsexpr->output; + JsonReturning *returning; + + returning = transformJsonReturning(pstate, output, "JSON_SCALAR()"); + + if (exprType(arg) == UNKNOWNOID) + arg = coerce_to_specific_type(pstate, arg, TEXTOID, "JSON_SCALAR"); + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SCALAR, list_make1(arg), NULL, + returning, false, false, jsexpr->location); +} + +/* + * Transform a JSON_SERIALIZE() expression. + */ +static Node * +transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) +{ + Node *arg = transformJsonValueExpr(pstate, "JSON_SERIALIZE()", + expr->expr, + JS_FORMAT_JSON, + InvalidOid); + JsonReturning *returning; + + if (expr->output) + { + returning = transformJsonOutput(pstate, expr->output, true); + + if (returning->typid != BYTEAOID) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(returning->typid, &typcategory, + &typispreferred); + if (typcategory != TYPCATEGORY_STRING) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), + "JSON_SERIALIZE()"), + errhint("Try returning a string type or bytea."))); + } + } + else + { + /* RETURNING TEXT FORMAT JSON is by default */ + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(JS_FORMAT_JSON, JS_ENC_DEFAULT, -1); + returning->typid = TEXTOID; + returning->typmod = -1; + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), + NULL, returning, false, false, expr->location); +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4cca97ff9c..520d4f2a23 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1953,6 +1953,15 @@ FigureColnameInternal(Node *node, char **name) /* make XMLSERIALIZE act like a regular function */ *name = "xmlserialize"; return 2; + case T_JsonParseExpr: + *name = "json"; + return 2; + case T_JsonScalarExpr: + *name = "json_scalar"; + return 2; + case T_JsonSerializeExpr: + *name = "json_serialize"; + return 2; case T_JsonObjectConstructor: /* make JSON_OBJECT act like a regular function */ *name = "json_object"; diff --git a/src/backend/utils/adt/format_type.c b/src/backend/utils/adt/format_type.c index 12402a0637..36c45a3978 100644 --- a/src/backend/utils/adt/format_type.c +++ b/src/backend/utils/adt/format_type.c @@ -294,6 +294,10 @@ format_type_extended(Oid type_oid, int32 typemod, bits16 flags) else buf = pstrdup("character varying"); break; + + case JSONOID: + buf = pstrdup("json"); + break; } if (buf == NULL) diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index f6bef9c148..c316f848e1 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -653,6 +653,20 @@ row_to_json_pretty(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); } +/* + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. + */ +Datum +to_json_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid) +{ + StringInfo result = makeStringInfo(); + + datum_to_json(val, false, result, tcategory, outfuncoid, false); + + return PointerGetDatum(cstring_to_text_with_len(result->data, result->len)); +} + /* * Is the given type immutable when coming out of a JSON context? * @@ -704,7 +718,6 @@ to_json(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - StringInfo result; JsonTypeCategory tcategory; Oid outfuncoid; @@ -716,11 +729,7 @@ to_json(PG_FUNCTION_ARGS) json_categorize_type(val_type, false, &tcategory, &outfuncoid); - result = makeStringInfo(); - - datum_to_json(val, false, result, tcategory, outfuncoid, false); - - PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); + PG_RETURN_DATUM(to_json_worker(val, tcategory, outfuncoid)); } /* diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index fc64f56868..06ba409e64 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -33,6 +33,7 @@ typedef struct JsonbInState { JsonbParseState *parseState; JsonbValue *res; + bool unique_keys; Node *escontext; } JsonbInState; @@ -45,7 +46,8 @@ typedef struct JsonbAggState Oid val_output_func; } JsonbAggState; -static inline Datum jsonb_from_cstring(char *json, int len, Node *escontext); +static inline Datum jsonb_from_cstring(char *json, int len, bool unique_keys, + Node *escontext); static bool checkStringLen(size_t len, Node *escontext); static JsonParseErrorType jsonb_in_object_start(void *pstate); static JsonParseErrorType jsonb_in_object_end(void *pstate); @@ -76,7 +78,7 @@ jsonb_in(PG_FUNCTION_ARGS) { char *json = PG_GETARG_CSTRING(0); - return jsonb_from_cstring(json, strlen(json), fcinfo->context); + return jsonb_from_cstring(json, strlen(json), false, fcinfo->context); } /* @@ -100,7 +102,7 @@ jsonb_recv(PG_FUNCTION_ARGS) else elog(ERROR, "unsupported jsonb version number %d", version); - return jsonb_from_cstring(str, nbytes, NULL); + return jsonb_from_cstring(str, nbytes, false, NULL); } /* @@ -141,6 +143,18 @@ jsonb_send(PG_FUNCTION_ARGS) PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); } +/* + * Note: exported for use in SQL/JSON executor functions. + */ +Datum +jsonb_from_text(text *js, bool unique_keys) +{ + return jsonb_from_cstring(VARDATA_ANY(js), + VARSIZE_ANY_EXHDR(js), + unique_keys, + NULL); +} + /* * Get the type name of a jsonb container. */ @@ -234,7 +248,7 @@ jsonb_typeof(PG_FUNCTION_ARGS) * instead of being thrown. */ static inline Datum -jsonb_from_cstring(char *json, int len, Node *escontext) +jsonb_from_cstring(char *json, int len, bool unique_keys, Node *escontext) { JsonLexContext *lex; JsonbInState state; @@ -244,6 +258,7 @@ jsonb_from_cstring(char *json, int len, Node *escontext) memset(&sem, 0, sizeof(sem)); lex = makeJsonLexContextCstringLen(json, len, GetDatabaseEncoding(), true); + state.unique_keys = unique_keys; state.escontext = escontext; sem.semstate = (void *) &state; @@ -280,6 +295,7 @@ jsonb_in_object_start(void *pstate) JsonbInState *_state = (JsonbInState *) pstate; _state->res = pushJsonbValue(&_state->parseState, WJB_BEGIN_OBJECT, NULL); + _state->parseState->unique_keys = _state->unique_keys; return JSON_SUCCESS; } @@ -1021,6 +1037,23 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, datum_to_jsonb(val, is_null, result, tcategory, outfuncoid, key_scalar); } + +/* + * Note: exported for use in SQL/JSON executor functions, which cache the + * value of tcategory and outfuncoid. + */ +Datum +to_jsonb_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid) +{ + JsonbInState result; + + memset(&result, 0, sizeof(JsonbInState)); + + datum_to_jsonb(val, false, &result, tcategory, outfuncoid, false); + + return JsonbPGetDatum(JsonbValueToJsonb(result.res)); +} + /* * Is the given type immutable when coming out of a JSONB context? * @@ -1072,7 +1105,6 @@ to_jsonb(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - JsonbInState result; JsonTypeCategory tcategory; Oid outfuncoid; @@ -1084,11 +1116,7 @@ to_jsonb(PG_FUNCTION_ARGS) json_categorize_type(val_type, true, &tcategory, &outfuncoid); - memset(&result, 0, sizeof(JsonbInState)); - - datum_to_jsonb(val, false, &result, tcategory, outfuncoid, false); - - PG_RETURN_POINTER(JsonbValueToJsonb(result.res)); + PG_RETURN_DATUM(to_jsonb_worker(val, tcategory, outfuncoid)); } Datum diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index fcb2f45f62..4ef9173c0b 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -10832,6 +10832,15 @@ get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, case JSCTOR_JSON_ARRAY: funcname = "JSON_ARRAY"; break; + case JSCTOR_JSON_PARSE: + funcname = "JSON"; + break; + case JSCTOR_JSON_SCALAR: + funcname = "JSON_SCALAR"; + break; + case JSCTOR_JSON_SERIALIZE: + funcname = "JSON_SERIALIZE"; + break; default: elog(ERROR, "invalid JsonConstructorType %d", ctor->type); } @@ -10879,7 +10888,10 @@ get_json_constructor_options(JsonConstructorExpr *ctor, StringInfo buf) if (ctor->unique) appendStringInfoString(buf, " WITH UNIQUE KEYS"); - get_json_returning(ctor->returning, buf, true); + if (!((ctor->type == JSCTOR_JSON_PARSE || + ctor->type == JSCTOR_JSON_SCALAR) && + ctor->returning->typid == JSONOID)) + get_json_returning(ctor->returning, buf, true); } /* diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index efb5c3e098..d926713bd9 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1716,6 +1716,17 @@ typedef struct TriggerTransition /* Nodes for SQL/JSON support */ +/* + * JsonQuotes - + * representation of [KEEP|OMIT] QUOTES clause for JSON_QUERY() + */ +typedef enum JsonQuotes +{ + JS_QUOTES_UNSPEC, /* unspecified */ + JS_QUOTES_KEEP, /* KEEP QUOTES */ + JS_QUOTES_OMIT /* OMIT QUOTES */ +} JsonQuotes; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1739,6 +1750,43 @@ typedef struct JsonKeyValue JsonValueExpr *value; /* JSON value expression */ } JsonKeyValue; +/* + * JsonParseExpr - + * untransformed representation of JSON() + */ +typedef struct JsonParseExpr +{ + NodeTag type; + JsonValueExpr *expr; /* string expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + bool unique_keys; /* WITH UNIQUE KEYS? */ + int location; /* token location, or -1 if unknown */ +} JsonParseExpr; + +/* + * JsonScalarExpr - + * untransformed representation of JSON_SCALAR() + */ +typedef struct JsonScalarExpr +{ + NodeTag type; + Expr *expr; /* scalar expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonScalarExpr; + +/* + * JsonSerializeExpr - + * untransformed representation of JSON_SERIALIZE() function + */ +typedef struct JsonSerializeExpr +{ + NodeTag type; + JsonValueExpr *expr; /* json value expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonSerializeExpr; + /* * JsonObjectConstructor - * untransformed representation of JSON_OBJECT() constructor diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index e1aadc39cf..4104371bd9 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1613,7 +1613,10 @@ typedef enum JsonConstructorType JSCTOR_JSON_OBJECT = 1, JSCTOR_JSON_ARRAY = 2, JSCTOR_JSON_OBJECTAGG = 3, - JSCTOR_JSON_ARRAYAGG = 4 + JSCTOR_JSON_ARRAYAGG = 4, + JSCTOR_JSON_SCALAR = 5, + JSCTOR_JSON_SERIALIZE = 6, + JSCTOR_JSON_PARSE = 7 } JsonConstructorType; /* diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index f5b2e61ca5..5984dcfa4b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -230,11 +230,13 @@ PG_KEYWORD("is", IS, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("isnull", ISNULL, TYPE_FUNC_NAME_KEYWORD, AS_LABEL) PG_KEYWORD("isolation", ISOLATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) -PG_KEYWORD("json", JSON, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index 649a1644f2..f928e6142a 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -368,7 +368,6 @@ typedef struct JsonbIterator struct JsonbIterator *parent; } JsonbIterator; - /* Convenience macros */ static inline Jsonb * DatumGetJsonbP(Datum d) @@ -418,6 +417,7 @@ extern void JsonbHashScalarValueExtended(const JsonbValue *scalarVal, uint64 *hash, uint64 seed); /* jsonb.c support functions */ +extern Datum jsonb_from_text(text *js, bool unique_keys); extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index 121dfd5d24..5ee1964096 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -82,5 +82,9 @@ typedef enum extern void json_categorize_type(Oid typoid, bool is_jsonb, JsonTypeCategory *tcategory, Oid *outfuncoid); +extern Datum to_json_worker(Datum val, JsonTypeCategory tcategory, + Oid outfuncoid); +extern Datum to_jsonb_worker(Datum val, JsonTypeCategory tcategory, + Oid outfuncoid); #endif diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index d73c7e2c6c..d5074d73a2 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -1,3 +1,293 @@ +-- JSON() +SELECT JSON(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON(); + ^ +SELECT JSON(NULL); + json +------ + +(1 row) + +SELECT JSON('{ "a" : 1 } '); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +ERROR: JSON ENCODING clause is only allowed for bytea input type +LINE 1: SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); + ^ +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + pg_typeof +----------- + json +(1 row) + +SELECT JSON(' 1 '::json); + json +--------- + 1 +(1 row) + +SELECT JSON(' 1 '::jsonb); + json +------ + 1 +(1 row) + +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +ERROR: cannot use non-string types with WITH UNIQUE KEYS clause +LINE 1: SELECT JSON(' 1 '::json WITH UNIQUE KEYS); + ^ +SELECT JSON(123); +ERROR: cannot cast type integer to json +LINE 1: SELECT JSON(123); + ^ +SELECT JSON('{"a": 1, "a": 2}'); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +ERROR: duplicate JSON object key value +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); + QUERY PLAN +----------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); + QUERY PLAN +------------------------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON ENCODING UTF8) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); + QUERY PLAN +---------------------------------------------- + Result + Output: JSON('123'::text WITH UNIQUE KEYS) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +SELECT pg_typeof(JSON('123')); + pg_typeof +----------- + json +(1 row) + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SCALAR(); + ^ +SELECT JSON_SCALAR(NULL); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(NULL::int); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(123); + json_scalar +------------- + 123 +(1 row) + +SELECT JSON_SCALAR(123.45); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(123.45::numeric); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(true); + json_scalar +------------- + true +(1 row) + +SELECT JSON_SCALAR(false); + json_scalar +------------- + false +(1 row) + +SELECT JSON_SCALAR(' 123.45'); + json_scalar +------------- + " 123.45" +(1 row) + +SELECT JSON_SCALAR('2020-06-07'::date); + json_scalar +-------------- + "2020-06-07" +(1 row) + +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); + json_scalar +----------------------- + "2020-06-07T01:02:03" +(1 row) + +SELECT JSON_SCALAR('{}'::json); + json_scalar +------------- + {} +(1 row) + +SELECT JSON_SCALAR('{}'::jsonb); + json_scalar +------------- + {} +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); + QUERY PLAN +---------------------------- + Result + Output: JSON_SCALAR(123) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); + QUERY PLAN +------------------------------------ + Result + Output: JSON_SCALAR('123'::text) +(2 rows) + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SERIALIZE(); + ^ +SELECT JSON_SERIALIZE(NULL); + json_serialize +---------------- + +(1 row) + +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } '); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('1'); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('1' FORMAT JSON); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); + json_serialize +---------------------------- + \x7b20226122203a2031207d20 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + pg_typeof +----------- + text +(1 row) + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); +ERROR: cannot use RETURNING type jsonb in JSON_SERIALIZE() +HINT: Try returning a string type or bytea. +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); + QUERY PLAN +----------------------------------------------------- + Result + Output: JSON_SERIALIZE('{}'::json RETURNING text) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + QUERY PLAN +------------------------------------------------------ + Result + Output: JSON_SERIALIZE('{}'::json RETURNING bytea) +(2 rows) + -- JSON_OBJECT() SELECT JSON_OBJECT(); json_object @@ -630,6 +920,13 @@ ERROR: duplicate JSON object key SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); ERROR: duplicate JSON object key +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + json_objectagg +------------------ + {"1": 1, "2": 2} +(1 row) + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -645,6 +942,41 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); CREATE OR REPLACE VIEW public.json_object_view AS SELECT JSON_OBJECT('foo' : '1'::text FORMAT JSON, 'bar' : 'baz'::text RETURNING json) AS "json_object" DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + a | json_objectagg +---------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(2 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":1,"v":null} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1, "2" : 2 } + {"k":1,"v":null} | { "1" : 1, "2" : 2 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 4fd820fd51..e6e20175b0 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -1,3 +1,67 @@ +-- JSON() +SELECT JSON(); +SELECT JSON(NULL); +SELECT JSON('{ "a" : 1 } '); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + +SELECT JSON(' 1 '::json); +SELECT JSON(' 1 '::jsonb); +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +SELECT JSON(123); + +SELECT JSON('{"a": 1, "a": 2}'); +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +SELECT pg_typeof(JSON('123')); + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +SELECT JSON_SCALAR(NULL); +SELECT JSON_SCALAR(NULL::int); +SELECT JSON_SCALAR(123); +SELECT JSON_SCALAR(123.45); +SELECT JSON_SCALAR(123.45::numeric); +SELECT JSON_SCALAR(true); +SELECT JSON_SCALAR(false); +SELECT JSON_SCALAR(' 123.45'); +SELECT JSON_SCALAR('2020-06-07'::date); +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); +SELECT JSON_SCALAR('{}'::json); +SELECT JSON_SCALAR('{}'::jsonb); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +SELECT JSON_SERIALIZE(NULL); +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); +SELECT JSON_SERIALIZE('{ "a" : 1 } '); +SELECT JSON_SERIALIZE('1'); +SELECT JSON_SERIALIZE('1' FORMAT JSON); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); + + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + -- JSON_OBJECT() SELECT JSON_OBJECT(); SELECT JSON_OBJECT(RETURNING json); @@ -216,6 +280,9 @@ FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -227,6 +294,24 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 05814136c6..8efc61cb55 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1296,6 +1296,7 @@ JsonPathPredicateCallback JsonPathString JsonReturning JsonSemAction +JsonSerializeExpr JsonTokenType JsonTransformStringValuesAction JsonTypeCategory -- 2.35.3
v9-0005-Claim-SQL-standard-compliance-for-SQL-JSON-featur.patch (application/octet-stream, 2.3K) download From 008b12e008f17afe4b540f6c784773bd0acd8935 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Tue, 18 Jul 2023 17:58:44 +0900 Subject: [PATCH v9 5/5] Claim SQL standard compliance for SQL/JSON features Discussion: https://postgr.es/m/d03d809c-d0fb-fd6a-1476-d6dc18ec940e@dunslane.net --- src/backend/catalog/sql_features.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt index b33065d7bf..b379c71df9 100644 --- a/src/backend/catalog/sql_features.txt +++ b/src/backend/catalog/sql_features.txt @@ -548,15 +548,15 @@ T811 Basic SQL/JSON constructor functions YES T812 SQL/JSON: JSON_OBJECTAGG YES T813 SQL/JSON: JSON_ARRAYAGG with ORDER BY YES T814 Colon in JSON_OBJECT or JSON_OBJECTAGG YES -T821 Basic SQL/JSON query operators NO +T821 Basic SQL/JSON query operators YES T822 SQL/JSON: IS JSON WITH UNIQUE KEYS predicate YES -T823 SQL/JSON: PASSING clause NO -T824 JSON_TABLE: specific PLAN clause NO -T825 SQL/JSON: ON EMPTY and ON ERROR clauses NO -T826 General value expression in ON ERROR or ON EMPTY clauses NO -T827 JSON_TABLE: sibling NESTED COLUMNS clauses NO -T828 JSON_QUERY NO -T829 JSON_QUERY: array wrapper options NO +T823 SQL/JSON: PASSING clause YES +T824 JSON_TABLE: specific PLAN clause YES +T825 SQL/JSON: ON EMPTY and ON ERROR clauses YES +T826 General value expression in ON ERROR or ON EMPTY clauses YES +T827 JSON_TABLE: sibling NESTED COLUMNS clauses YES +T828 JSON_QUERY YES +T829 JSON_QUERY: array wrapper options YES T830 Enforcing unique keys in SQL/JSON constructor functions YES T831 SQL/JSON path language: strict mode YES T832 SQL/JSON path language: item method YES @@ -565,7 +565,7 @@ T834 SQL/JSON path language: wildcard member accessor YES T835 SQL/JSON path language: filter expressions YES T836 SQL/JSON path language: starts with predicate YES T837 SQL/JSON path language: regex_like predicate YES -T838 JSON_TABLE: PLAN DEFAULT clause NO +T838 JSON_TABLE: PLAN DEFAULT clause YES T839 Formatted cast of datetimes to/from character strings NO T840 Hex integer literals in SQL/JSON path language YES T851 SQL/JSON: optional keywords for default syntax YES -- 2.35.3
v9-0004-JSON_TABLE.patch (application/octet-stream, 164.2K) download From 41316f496eb8393327a650e9941c1b60d0c0d3a7 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Tue, 18 Jul 2023 17:58:39 +0900 Subject: [PATCH v9 4/5] JSON_TABLE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This feature allows jsonb data to be treated as a table and thus used in a FROM clause like other tabular data. Data can be selected from the jsonb using jsonpath expressions, and hoisted out of nested structures in the jsonb to form multiple rows, more or less like an outer join. This also adds the PLAN clauses for JSON_TABLE, which allow the user to specify how data from nested paths are joined, allowing considerable freedom in shaping the tabular output of JSON_TABLE. PLAN DEFAULT allows the user to specify the global strategies when dealing with sibling or child nested paths. The is often sufficient to achieve the necessary goal, and is considerably simpler than the full PLAN clause, which allows the user to specify the strategy to be used for each named nested path. Author: Nikita Glukhov <n.gluhov@postgrespro.ru> Author: Teodor Sigaev <teodor@sigaev.ru> Author: Oleg Bartunov <obartunov@gmail.com> Author: Alexander Korotkov <aekorotkov@gmail.com> Author: Andrew Dunstan <andrew@dunslane.net> Author: Amit Langote <amitlangote09@gmail.com> Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- doc/src/sgml/func.sgml | 496 +++++++++ src/backend/commands/explain.c | 8 +- src/backend/executor/execExprInterp.c | 5 + src/backend/executor/nodeTableFuncscan.c | 27 +- src/backend/nodes/makefuncs.c | 19 + src/backend/nodes/nodeFuncs.c | 30 + src/backend/parser/Makefile | 1 + src/backend/parser/gram.y | 476 ++++++++- src/backend/parser/meson.build | 1 + src/backend/parser/parse_clause.c | 14 +- src/backend/parser/parse_expr.c | 35 +- src/backend/parser/parse_jsontable.c | 741 +++++++++++++ src/backend/parser/parse_relation.c | 5 +- src/backend/parser/parse_target.c | 3 + src/backend/utils/adt/jsonpath_exec.c | 543 ++++++++++ src/backend/utils/adt/ruleutils.c | 279 ++++- src/include/nodes/execnodes.h | 2 + src/include/nodes/makefuncs.h | 2 + src/include/nodes/parsenodes.h | 90 ++ src/include/nodes/primnodes.h | 61 +- src/include/parser/kwlist.h | 4 + src/include/parser/parse_clause.h | 3 + src/include/utils/jsonpath.h | 3 + src/test/regress/expected/json_sqljson.out | 6 + src/test/regress/expected/jsonb_sqljson.out | 1069 +++++++++++++++++++ src/test/regress/sql/json_sqljson.sql | 4 + src/test/regress/sql/jsonb_sqljson.sql | 629 +++++++++++ src/tools/pgindent/typedefs.list | 13 + 28 files changed, 4536 insertions(+), 33 deletions(-) create mode 100644 src/backend/parser/parse_jsontable.c diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index ce001c4a03..05da73d829 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -17138,6 +17138,502 @@ array w/o UK? | t </sect2> + <sect2 id="functions-sqljson-table"> + <title>JSON_TABLE</title> + <indexterm> + <primary>json_table</primary> + </indexterm> + + <para> + <function>json_table</function> is an SQL/JSON function which + queries <acronym>JSON</acronym> data + and presents the results as a relational view, which can be accessed as a + regular SQL table. You can only use <function>json_table</function> inside the + <literal>FROM</literal> clause of a <literal>SELECT</literal> statement. + </para> + + <para> + Taking JSON data as input, <function>json_table</function> uses + a path expression to extract a part of the provided data that + will be used as a <firstterm>row pattern</firstterm> for the + constructed view. Each SQL/JSON item at the top level of the row pattern serves + as the source for a separate row in the constructed relational view. + </para> + + <para> + To split the row pattern into columns, <function>json_table</function> + provides the <literal>COLUMNS</literal> clause that defines the + schema of the created view. For each column to be constructed, + this clause provides a separate path expression that evaluates + the row pattern, extracts a JSON item, and returns it as a + separate SQL value for the specified column. If the required value + is stored in a nested level of the row pattern, it can be extracted + using the <literal>NESTED PATH</literal> subclause. Joining the + columns returned by <literal>NESTED PATH</literal> can add multiple + new rows to the constructed view. Such rows are called + <firstterm>child rows</firstterm>, as opposed to the <firstterm>parent row</firstterm> + that generates them. + </para> + + <para> + The rows produced by <function>JSON_TABLE</function> are laterally + joined to the row that generated them, so you do not have to explicitly join + the constructed view with the original table holding <acronym>JSON</acronym> + data. Optionally, you can specify how to join the columns returned + by <literal>NESTED PATH</literal> using the <literal>PLAN</literal> clause. + </para> + + <para> + Each <literal>NESTED PATH</literal> clause can generate one or more + columns. Columns produced by <literal>NESTED PATH</literal>s at the + same level are considered to be <firstterm>siblings</firstterm>, + while a column produced by a <literal>NESTED PATH</literal> is + considered to be a child of the column produced by a + <literal>NESTED PATH</literal> or row expression at a higher level. + Sibling columns are always joined first. Once they are processed, + the resulting rows are joined to the parent row. + </para> + + <para> + The syntax is: + </para> + +<synopsis> +JSON_TABLE ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> AS <replaceable>json_path_name</replaceable> </optional> <optional> PASSING { <replaceable>value</replaceable> AS <replaceable>varname</replaceable> } <optional>, ...</optional> </optional> + COLUMNS ( <replaceable class="parameter">json_table_column</replaceable> <optional>, ...</optional> ) + <optional> { <literal>ERROR</literal> | <literal>EMPTY</literal> } <literal>ON ERROR</literal> </optional> + <optional> + PLAN ( <replaceable class="parameter">json_table_plan</replaceable> ) | + PLAN DEFAULT ( { INNER | OUTER } <optional> , { CROSS | UNION } </optional> + | { CROSS | UNION } <optional> , { INNER | OUTER } </optional> ) + </optional> +) + +<phrase> +where <replaceable class="parameter">json_table_column</replaceable> is: +</phrase> + <replaceable>name</replaceable> <replaceable>type</replaceable> <optional> PATH <replaceable>json_path_specification</replaceable> </optional> + <optional> { WITHOUT | WITH { CONDITIONAL | <optional>UNCONDITIONAL</optional> } } <optional> ARRAY </optional> WRAPPER </optional> + <optional> { KEEP | OMIT } QUOTES <optional> ON SCALAR STRING </optional> </optional> + <optional> { ERROR | NULL | DEFAULT <replaceable>expression</replaceable> } ON EMPTY </optional> + <optional> { ERROR | NULL | DEFAULT <replaceable>expression</replaceable> } ON ERROR </optional> + | <replaceable>name</replaceable> <replaceable>type</replaceable> FORMAT <replaceable>json_representation</replaceable> + <optional> PATH <replaceable>json_path_specification</replaceable> </optional> + <optional> { WITHOUT | WITH { CONDITIONAL | <optional>UNCONDITIONAL</optional> } } <optional> ARRAY </optional> WRAPPER </optional> + <optional> { KEEP | OMIT } QUOTES <optional> ON SCALAR STRING </optional> </optional> + <optional> { ERROR | NULL | EMPTY { ARRAY | OBJECT } | DEFAULT <replaceable>expression</replaceable> } ON EMPTY </optional> + <optional> { ERROR | NULL | EMPTY { ARRAY | OBJECT } | DEFAULT <replaceable>expression</replaceable> } ON ERROR </optional> + | <replaceable>name</replaceable> <replaceable>type</replaceable> EXISTS <optional> PATH <replaceable>json_path_specification</replaceable> </optional> + <optional> { ERROR | TRUE | FALSE | UNKNOWN } ON ERROR </optional> + | NESTED PATH <replaceable>json_path_specification</replaceable> <optional> AS <replaceable>path_name</replaceable> </optional> + COLUMNS ( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + | <replaceable>name</replaceable> FOR ORDINALITY +<phrase> +<replaceable>json_table_plan</replaceable> is: +</phrase> + <replaceable>json_path_name</replaceable> <optional> { OUTER | INNER } <replaceable>json_table_plan_primary</replaceable> </optional> + | <replaceable>json_table_plan_primary</replaceable> { UNION <replaceable>json_table_plan_primary</replaceable> } <optional>...</optional> + | <replaceable>json_table_plan_primary</replaceable> { CROSS <replaceable>json_table_plan_primary</replaceable> } <optional>...</optional> +<phrase> +<replaceable>json_table_plan_primary</replaceable> is: +</phrase> + <replaceable>json_path_name</replaceable> | ( <replaceable>json_table_plan</replaceable> ) +</synopsis> + + <para> + Each syntax element is described below in more detail. + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional></literal> + </term> + <listitem> + <para> + The input data to query, the JSON path expression defining the query, + and an optional <literal>PASSING</literal> clause, which can provide data + values to the <replaceable>path_expression</replaceable>. + The result of the input data + evaluation is called the <firstterm>row pattern</firstterm>. The row + pattern is used as the source for row values in the constructed view. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>COLUMNS</literal>( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + The <literal>COLUMNS</literal> clause defining the schema of the + constructed view. In this clause, you must specify all the columns + to be filled with SQL/JSON items. + The <replaceable>json_table_column</replaceable> + expression has the following syntax variants: + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>name</replaceable> <replaceable>type</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional></literal> + </term> + <listitem> + + <para> + Inserts a single SQL/JSON item into each row of + the specified column. + </para> + <para> + The provided <literal>PATH</literal> expression is evaluated and + the column is filled with the produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>ON EMPTY</literal> and + <literal>ON ERROR</literal> clauses to define how to handle missing values + or structural errors. + <literal>WRAPPER</literal> and <literal>QUOTES</literal> clauses can only + be used with JSON, array, and composite types. + These clauses have the same syntax and semantics as for + <function>json_value</function> and <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> <literal>FORMAT</literal> <replaceable>json_representation</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a composite SQL/JSON + item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression is evaluated and + the column is filled with the produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>WRAPPER</literal>, <literal>QUOTES</literal>, + <literal>ON EMPTY</literal> and <literal>ON ERROR</literal> clauses + to define additional settings for the returned SQL/JSON items. + These clauses have the same syntax and semantics as + for <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> + <literal>EXISTS</literal> <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a boolean item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression is evaluated, + a check whether any SQL/JSON items were returned is done, and + the column is filled with the resulting boolean value, one for each row. + The specified <parameter>type</parameter> should have a cast from the + <type>boolean</type>. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + </para> + <para> + Optionally, you can add <literal>ON ERROR</literal> clause to define + error behavior. This clause has the same syntax and semantics as + for <function>json_exists</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>NESTED PATH</literal> <replaceable>json_path_specification</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> + <literal>COLUMNS</literal> ( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + Extracts SQL/JSON items from nested levels of the row pattern, + generates one or more columns as defined by the <literal>COLUMNS</literal> + subclause, and inserts the extracted SQL/JSON items into each row of these columns. + The <replaceable>json_table_column</replaceable> expression in the + <literal>COLUMNS</literal> subclause uses the same syntax as in the + parent <literal>COLUMNS</literal> clause. + </para> + + <para> + The <literal>NESTED PATH</literal> syntax is recursive, + so you can go down multiple nested levels by specifying several + <literal>NESTED PATH</literal> subclauses within each other. + It allows to unnest the hierarchy of JSON objects and arrays + in a single function invocation rather than chaining several + <function>JSON_TABLE</function> expressions in an SQL statement. + </para> + + <para> + You can use the <literal>PLAN</literal> clause to define how + to join the columns returned by <literal>NESTED PATH</literal> clauses. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <literal>FOR ORDINALITY</literal> + </term> + <listitem> + + <para> + Adds an ordinality column that provides sequential row numbering. + You can have only one ordinality column per table. Row numbering + is 1-based. For child rows that result from the <literal>NESTED PATH</literal> + clauses, the parent row number is repeated. + </para> + </listitem> + </varlistentry> + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>AS</literal> <replaceable>json_path_name</replaceable> + </term> + <listitem> + + <para> + The optional <replaceable>json_path_name</replaceable> serves as an + identifier of the provided <replaceable>json_path_specification</replaceable>. + The path name must be unique and distinct from the column names. + When using the <literal>PLAN</literal> clause, you must specify the names + for all the paths, including the row pattern. Each path name can appear in + the <literal>PLAN</literal> clause only once. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN</literal> ( <replaceable>json_table_plan</replaceable> ) + </term> + <listitem> + + <para> + Defines how to join the data returned by <literal>NESTED PATH</literal> + clauses to the constructed view. + </para> + <para> + To join columns with parent/child relationship, you can use: + </para> + <variablelist> + <varlistentry> + <term> + <literal>INNER</literal> + </term> + <listitem> + + <para> + Use <literal>INNER JOIN</literal>, so that the parent row + is omitted from the output if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>OUTER</literal> + </term> + <listitem> + + <para> + Use <literal>LEFT OUTER JOIN</literal>, so that the parent row + is always included into the output even if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>, with NULL values + inserted into the child columns if the corresponding + values are missing. + </para> + <para> + This is the default option for joining columns with parent/child relationship. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para> + To join sibling columns, you can use: + </para> + + <variablelist> + <varlistentry> + <term> + <literal>UNION</literal> + </term> + <listitem> + + <para> + Generate one row for each value produced by each of the sibling + columns. The columns from the other siblings are set to null. + </para> + <para> + This is the default option for joining sibling columns. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>CROSS</literal> + </term> + <listitem> + + <para> + Generate one row for each combination of values from the sibling columns. + </para> + </listitem> + </varlistentry> + + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN DEFAULT</literal> ( <literal><replaceable>OUTER | INNER</replaceable> <optional>, <replaceable>UNION | CROSS</replaceable> </optional></literal> ) + </term> + <listitem> + <para> + The terms can also be specified in reverse order. The + <literal>INNER</literal> or <literal>OUTER</literal> option defines the + joining plan for parent/child columns, while <literal>UNION</literal> or + <literal>CROSS</literal> affects joins of sibling columns. This form + of <literal>PLAN</literal> overrides the default plan for + all columns at once. Even though the path names are not included in the + <literal>PLAN DEFAULT</literal> form, to conform to the SQL/JSON standard + they must be provided for all the paths if the <literal>PLAN</literal> + clause is used. + </para> + <para> + <literal>PLAN DEFAULT</literal> is simpler than specifying a complete + <literal>PLAN</literal>, and is often all that is required to get the desired + output. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para>Examples</para> + + <para> + In these examples the following small table storing some JSON data will be used: +<programlisting> +CREATE TABLE my_films ( js jsonb ); + +INSERT INTO my_films VALUES ( +'{ "favorites" : [ + { "kind" : "comedy", "films" : [ + { "title" : "Bananas", + "director" : "Woody Allen"}, + { "title" : "The Dinner Game", + "director" : "Francis Veber" } ] }, + { "kind" : "horror", "films" : [ + { "title" : "Psycho", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "thriller", "films" : [ + { "title" : "Vertigo", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "drama", "films" : [ + { "title" : "Yojimbo", + "director" : "Akira Kurosawa" } ] } + ] }'); +</programlisting> + </para> + <para> + Query the <structname>my_films</structname> table holding + some JSON data about the films and create a view that + distributes the film genre, title, and director between separate columns: +<screen> +SELECT jt.* FROM + my_films, + JSON_TABLE ( js, '$.favorites[*]' COLUMNS ( + id FOR ORDINALITY, + kind text PATH '$.kind', + NESTED PATH '$.films[*]' COLUMNS ( + title text PATH '$.title', + director text PATH '$.director'))) AS jt; +----+----------+------------------+------------------- + id | kind | title | director +----+----------+------------------+------------------- + 1 | comedy | Bananas | Woody Allen + 1 | comedy | The Dinner Game | Francis Veber + 2 | horror | Psycho | Alfred Hitchcock + 3 | thriller | Vertigo | Alfred Hitchcock + 4 | drama | Yojimbo | Akira Kurosawa + (5 rows) +</screen> + </para> + + <para> + Find a director that has done films in two different genres: +<screen> +SELECT + director1 AS director, title1, kind1, title2, kind2 +FROM + my_films, + JSON_TABLE ( js, '$.favorites' AS favs COLUMNS ( + NESTED PATH '$[*]' AS films1 COLUMNS ( + kind1 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film1 COLUMNS ( + title1 text PATH '$.title', + director1 text PATH '$.director') + ), + NESTED PATH '$[*]' AS films2 COLUMNS ( + kind2 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film2 COLUMNS ( + title2 text PATH '$.title', + director2 text PATH '$.director' + ) + ) + ) + PLAN (favs OUTER ((films1 INNER film1) CROSS (films2 INNER film2))) + ) AS jt + WHERE kind1 > kind2 AND director1 = director2; + + director | title1 | kind1 | title2 | kind2 +------------------+---------+----------+--------+-------- + Alfred Hitchcock | Vertigo | thriller | Psycho | horror +(1 row) +</screen> + </para> + </sect2> + <sect2 id="functions-sqljson-path"> <title>The SQL/JSON Path Language</title> diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 8570b14f62..ad899de5d6 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -3862,7 +3862,13 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es) break; case T_TableFuncScan: Assert(rte->rtekind == RTE_TABLEFUNC); - objectname = "xmltable"; + if (rte->tablefunc) + if (rte->tablefunc->functype == TFT_XMLTABLE) + objectname = "xmltable"; + else /* Must be TFT_JSON_TABLE */ + objectname = "json_table"; + else + objectname = NULL; objecttag = "Table Function Name"; break; case T_ValuesScan: diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 4c97e714ea..84c8730129 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -4309,6 +4309,11 @@ ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) break; } + case JSON_TABLE_OP: + res = item; + resnull = false; + break; + default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); *op->resnull = true; diff --git a/src/backend/executor/nodeTableFuncscan.c b/src/backend/executor/nodeTableFuncscan.c index 791cbd2372..085a612533 100644 --- a/src/backend/executor/nodeTableFuncscan.c +++ b/src/backend/executor/nodeTableFuncscan.c @@ -28,6 +28,7 @@ #include "miscadmin.h" #include "nodes/execnodes.h" #include "utils/builtins.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/xml.h" @@ -161,8 +162,9 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) scanstate->ss.ps.qual = ExecInitQual(node->scan.plan.qual, &scanstate->ss.ps); - /* Only XMLTABLE is supported currently */ - scanstate->routine = &XmlTableRoutine; + /* Only XMLTABLE and JSON_TABLE are supported currently */ + scanstate->routine = + tf->functype == TFT_XMLTABLE ? &XmlTableRoutine : &JsonbTableRoutine; scanstate->perTableCxt = AllocSetContextCreate(CurrentMemoryContext, @@ -182,6 +184,10 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) ExecInitExprList(tf->colexprs, (PlanState *) scanstate); scanstate->coldefexprs = ExecInitExprList(tf->coldefexprs, (PlanState *) scanstate); + scanstate->colvalexprs = + ExecInitExprList(tf->colvalexprs, (PlanState *) scanstate); + scanstate->passingvalexprs = + ExecInitExprList(tf->passingvalexprs, (PlanState *) scanstate); scanstate->notnulls = tf->notnulls; @@ -381,14 +387,17 @@ tfuncInitialize(TableFuncScanState *tstate, ExprContext *econtext, Datum doc) routine->SetNamespace(tstate, ns_name, ns_uri); } - /* Install the row filter expression into the table builder context */ - value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); - if (isnull) - ereport(ERROR, - (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), - errmsg("row filter expression must not be null"))); + if (routine->SetRowFilter) + { + /* Install the row filter expression into the table builder context */ + value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); + if (isnull) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("row filter expression must not be null"))); - routine->SetRowFilter(tstate, TextDatumGetCString(value)); + routine->SetRowFilter(tstate, TextDatumGetCString(value)); + } /* * Install the column filter expressions into the table builder context. diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 9c02634355..dd5e17c6e9 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -875,6 +875,25 @@ makeJsonBehavior(JsonBehaviorType type, Node *default_expr) return behavior; } +/* + * makeJsonTableJoinedPlan - + * creates a joined JsonTablePlan node + */ +Node * +makeJsonTableJoinedPlan(JsonTablePlanJoinType type, Node *plan1, Node *plan2, + int location) +{ + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_JOINED; + n->join_type = type; + n->plan1 = castNode(JsonTablePlan, plan1); + n->plan2 = castNode(JsonTablePlan, plan2); + n->location = location; + + return (Node *) n; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index cd5fff2bbe..64a4fbf093 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -2629,6 +2629,10 @@ expression_tree_walker_impl(Node *node, return true; if (WALK(tf->coldefexprs)) return true; + if (WALK(tf->colvalexprs)) + return true; + if (WALK(tf->passingvalexprs)) + return true; } break; default: @@ -3697,6 +3701,8 @@ expression_tree_mutator_impl(Node *node, MUTATE(newnode->rowexpr, tf->rowexpr, Node *); MUTATE(newnode->colexprs, tf->colexprs, List *); MUTATE(newnode->coldefexprs, tf->coldefexprs, List *); + MUTATE(newnode->colvalexprs, tf->colvalexprs, List *); + MUTATE(newnode->passingvalexprs, tf->passingvalexprs, List *); return (Node *) newnode; } break; @@ -4128,6 +4134,30 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonTable: + { + JsonTable *jt = (JsonTable *) node; + + if (WALK(jt->common)) + return true; + if (WALK(jt->columns)) + return true; + } + break; + case T_JsonTableColumn: + { + JsonTableColumn *jtc = (JsonTableColumn *) node; + + if (WALK(jtc->typeName)) + return true; + if (WALK(jtc->on_empty)) + return true; + if (WALK(jtc->on_error)) + return true; + if (jtc->coltype == JTC_NESTED && WALK(jtc->columns)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile index 9f1c4022bb..f4c0cc7f10 100644 --- a/src/backend/parser/Makefile +++ b/src/backend/parser/Makefile @@ -23,6 +23,7 @@ OBJS = \ parse_enr.o \ parse_expr.o \ parse_func.o \ + parse_jsontable.o \ parse_merge.o \ parse_node.o \ parse_oper.o \ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 1736ba35bd..4cd70ca05e 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -654,19 +654,43 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_aggregate_func json_api_common_syntax json_argument + json_table + json_table_column_definition + json_table_ordinality_column_definition + json_table_regular_column_definition + json_table_formatted_column_definition + json_table_exists_column_definition + json_table_nested_columns + json_table_plan_clause_opt + json_table_plan + json_table_plan_simple + json_table_plan_parent_child + json_table_plan_outer + json_table_plan_inner + json_table_plan_sibling + json_table_plan_union + json_table_plan_cross + json_table_plan_primary %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt json_arguments + json_table_columns_clause + json_table_column_definition_list +%type <str> json_table_column_path_specification_clause_opt %type <ival> json_encoding_clause_opt json_predicate_type_constraint json_wrapper_behavior + json_table_default_plan_choices + json_table_default_plan_inner_outer + json_table_default_plan_union_cross %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt %type <jsbehavior> json_value_behavior json_query_behavior json_exists_behavior + json_table_behavior %type <js_quotes> json_quotes_clause_opt /* @@ -732,7 +756,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG - JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_TABLE JSON_VALUE KEY KEYS KEEP @@ -743,8 +767,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE - NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE - NORMALIZE NORMALIZED + NAME_P NAMES NATIONAL NATURAL NCHAR NESTED NEW NEXT NFC NFD NFKC NFKD NO + NONE NORMALIZE NORMALIZED NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC @@ -752,8 +776,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER - PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD - PLACING PLANS POLICY + PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH + PLACING PLAN PLANS POLICY POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION @@ -861,6 +885,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); * Using the same precedence as IDENT seems right for the reasons given above. */ %nonassoc UNBOUNDED /* ideally would have same precedence as IDENT */ +%nonassoc COLUMNS %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' @@ -883,6 +908,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL +%nonassoc json_table_column +%nonassoc NESTED +%left PATH %% /* @@ -13333,6 +13361,21 @@ table_ref: relation_expr opt_alias_clause $2->alias = $4; $$ = (Node *) $2; } + | json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $1); + + jt->alias = $2; + $$ = (Node *) jt; + } + | LATERAL_P json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $2); + + jt->alias = $3; + jt->lateral = true; + $$ = (Node *) jt; + } ; @@ -13900,6 +13943,8 @@ xmltable_column_option_el: { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(true), @1); } | NULL_P { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(false), @1); } + | PATH b_expr + { $$ = makeDefElem("path", $2, @1); } ; xml_namespace_list: @@ -16707,6 +16752,11 @@ json_value_behavior: | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } ; +json_table_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + ; + /* ARRAY is a noise word */ json_wrapper_behavior: WITHOUT WRAPPER { $$ = JSW_NONE; } @@ -16728,6 +16778,414 @@ json_quotes_clause_opt: | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } ; +json_table: + JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + json_table_behavior ON ERROR_P + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_columns_clause: + COLUMNS '(' json_table_column_definition_list ')' { $$ = $3; } + ; + +json_table_column_definition_list: + json_table_column_definition + { $$ = list_make1($1); } + | json_table_column_definition_list ',' json_table_column_definition + { $$ = lappend($1, $3); } + ; + +json_table_column_definition: + json_table_ordinality_column_definition %prec json_table_column + | json_table_regular_column_definition %prec json_table_column + | json_table_formatted_column_definition %prec json_table_column + | json_table_exists_column_definition %prec json_table_column + | json_table_nested_columns + ; + +json_table_ordinality_column_definition: + ColId FOR ORDINALITY + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FOR_ORDINALITY; + n->name = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_regular_column_definition: + ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_exists_column_definition: + ColId Typename + EXISTS json_table_column_path_specification_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + EXISTS json_table_column_path_specification_clause_opt + json_exists_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_column_path_specification_clause_opt: + PATH Sconst { $$ = $2; } + | /* EMPTY */ %prec json_table_column { $$ = NULL; } + ; + +json_table_formatted_column_definition: + ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->on_error = $12; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_nested_columns: + NESTED path_opt Sconst + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = NULL; + n->columns = $4; + n->location = @1; + $$ = (Node *) n; + } + | NESTED path_opt Sconst AS name + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = $5; + n->columns = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +path_opt: + PATH { } + | /* EMPTY */ { } + ; + +json_table_plan_clause_opt: + PLAN '(' json_table_plan ')' { $$ = $3; } + | PLAN DEFAULT '(' json_table_default_plan_choices ')' + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_DEFAULT; + n->join_type = $4; + n->location = @1; + $$ = (Node *) n; + } + | /* EMPTY */ { $$ = NULL; } + ; + +json_table_plan: + json_table_plan_simple + | json_table_plan_parent_child + | json_table_plan_sibling + ; + +json_table_plan_simple: + name + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_SIMPLE; + n->pathname = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_plan_primary: + json_table_plan_simple { $$ = $1; } + | '(' json_table_plan ')' + { + castNode(JsonTablePlan, $2)->location = @1; + $$ = $2; + } + ; + +json_table_plan_outer: + json_table_plan_simple OUTER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_OUTER, $1, $3, @1); } + ; + +json_table_plan_inner: + json_table_plan_simple INNER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_INNER, $1, $3, @1); } + ; + +json_table_plan_parent_child: + json_table_plan_outer + | json_table_plan_inner + ; + +json_table_plan_union: + json_table_plan_primary UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + | json_table_plan_union UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + ; + +json_table_plan_cross: + json_table_plan_primary CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + | json_table_plan_cross CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + ; + +json_table_plan_sibling: + json_table_plan_union + | json_table_plan_cross + ; + +json_table_default_plan_choices: + json_table_default_plan_inner_outer { $$ = $1 | JSTPJ_UNION; } + | json_table_default_plan_union_cross { $$ = $1 | JSTPJ_OUTER; } + | json_table_default_plan_inner_outer ',' + json_table_default_plan_union_cross { $$ = $1 | $3; } + | json_table_default_plan_union_cross ',' + json_table_default_plan_inner_outer { $$ = $1 | $3; } + ; + +json_table_default_plan_inner_outer: + INNER_P { $$ = JSTPJ_INNER; } + | OUTER_P { $$ = JSTPJ_OUTER; } + ; + +json_table_default_plan_union_cross: + UNION { $$ = JSTPJ_UNION; } + | CROSS { $$ = JSTPJ_CROSS; } + ; + json_returning_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17452,6 +17910,7 @@ unreserved_keyword: | MOVE | NAME_P | NAMES + | NESTED | NEW | NEXT | NFC @@ -17486,6 +17945,8 @@ unreserved_keyword: | PARTITION | PASSING | PASSWORD + | PATH + | PLAN | PLANS | POLICY | PRECEDING @@ -17650,6 +18111,7 @@ col_name_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | LEAST | NATIONAL @@ -18018,6 +18480,7 @@ bare_label_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | KEEP | KEY @@ -18057,6 +18520,7 @@ bare_label_keyword: | NATIONAL | NATURAL | NCHAR + | NESTED | NEW | NEXT | NFC @@ -18101,7 +18565,9 @@ bare_label_keyword: | PARTITION | PASSING | PASSWORD + | PATH | PLACING + | PLAN | PLANS | POLICY | POSITION diff --git a/src/backend/parser/meson.build b/src/backend/parser/meson.build index 94517a7f7d..0f0e4ac7ca 100644 --- a/src/backend/parser/meson.build +++ b/src/backend/parser/meson.build @@ -10,6 +10,7 @@ backend_sources += files( 'parse_enr.c', 'parse_expr.c', 'parse_func.c', + 'parse_jsontable.c', 'parse_merge.c', 'parse_node.c', 'parse_oper.c', diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index 334b9b42bd..affd812619 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -697,7 +697,11 @@ transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf) char **names; int colno; - /* Currently only XMLTABLE is supported */ + /* + * Currently we only support XMLTABLE here. See transformJsonTable() for + * JSON_TABLE support. + */ + tf->functype = TFT_XMLTABLE; constructName = "XMLTABLE"; docType = XMLOID; @@ -1104,13 +1108,17 @@ transformFromClauseItem(ParseState *pstate, Node *n, rtr->rtindex = nsitem->p_rtindex; return (Node *) rtr; } - else if (IsA(n, RangeTableFunc)) + else if (IsA(n, RangeTableFunc) || IsA(n, JsonTable)) { /* table function is like a plain relation */ RangeTblRef *rtr; ParseNamespaceItem *nsitem; - nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + if (IsA(n, RangeTableFunc)) + nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + else + nsitem = transformJsonTable(pstate, (JsonTable *) n); + *top_nsitem = nsitem; *namespace = list_make1(nsitem); rtr = makeNode(RangeTblRef); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index ca60b5604a..2913e24926 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4232,7 +4232,7 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) JsonFormatType format; char *constructName; - if (func->common->pathname) + if (func->common->pathname && func->op != JSON_TABLE_OP) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("JSON_TABLE path name is not allowed here"), @@ -4251,6 +4251,9 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) case JSON_EXISTS_OP: constructName = "JSON_EXISTS()"; break; + case JSON_TABLE_OP: + constructName = "JSON_TABLE()"; + break; default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); break; @@ -4290,14 +4293,19 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) &jsexpr->passing_values, &jsexpr->passing_names); - if (func->op != JSON_EXISTS_OP) + if (func->op != JSON_EXISTS_OP && func->op != JSON_TABLE_OP) jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, JSON_BEHAVIOR_NULL); - jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, - func->op == JSON_EXISTS_OP ? - JSON_BEHAVIOR_FALSE : - JSON_BEHAVIOR_NULL); + if (func->op == JSON_EXISTS_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_FALSE); + else if (func->op == JSON_TABLE_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_EMPTY); + else + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_NULL); return jsexpr; } @@ -4621,6 +4629,21 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->result_coercion->expr = NULL; } break; + + case JSON_TABLE_OP: + jsexpr->returning = makeNode(JsonReturning); + jsexpr->returning->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + jsexpr->returning->typid = exprType(contextItemExpr); + jsexpr->returning->typmod = -1; + + if (jsexpr->returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("JSON_TABLE() is not yet implemented for the json type"), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + break; } if (exprType(contextItemExpr) != JSONBOID) diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c new file mode 100644 index 0000000000..34e9f16f5c --- /dev/null +++ b/src/backend/parser/parse_jsontable.c @@ -0,0 +1,741 @@ +/*------------------------------------------------------------------------- + * + * parse_jsontable.c + * parsing of JSON_TABLE + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/parser/parse_jsontable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/pg_collation.h" +#include "catalog/pg_type.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "optimizer/optimizer.h" +#include "parser/parse_clause.h" +#include "parser/parse_collate.h" +#include "parser/parse_expr.h" +#include "parser/parse_relation.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/json.h" +#include "utils/lsyscache.h" + +/* Context for JSON_TABLE transformation */ +typedef struct JsonTableParseContext +{ + ParseState *pstate; /* parsing state */ + JsonTable *table; /* untransformed node */ + TableFunc *tablefunc; /* transformed node */ + List *pathNames; /* list of all path and columns names */ + int pathNameId; /* path name id counter */ + Oid contextItemTypid; /* type oid of context item (json/jsonb) */ +} JsonTableParseContext; + +static JsonTableParent *transformJsonTableColumns(JsonTableParseContext *cxt, + JsonTablePlan *plan, + List *columns, + char *pathSpec, + char **pathName, + int location); + +static Node * +makeStringConst(char *str, int location) +{ + A_Const *n = makeNode(A_Const); + + n->val.node.type = T_String; + n->val.sval.sval = str; + n->location = location; + + return (Node *) n; +} + +/* + * Transform JSON_TABLE column + * - regular column into JSON_VALUE() + * - FORMAT JSON column into JSON_QUERY() + * - EXISTS column into JSON_EXISTS() + */ +static Node * +transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, + List *passingArgs, bool errorOnError) +{ + JsonFuncExpr *jfexpr = makeNode(JsonFuncExpr); + JsonCommon *common = makeNode(JsonCommon); + JsonOutput *output = makeNode(JsonOutput); + char *pathspec; + JsonFormat *default_format; + + jfexpr->op = + jtc->coltype == JTC_REGULAR ? JSON_VALUE_OP : + jtc->coltype == JTC_EXISTS ? JSON_EXISTS_OP : JSON_QUERY_OP; + jfexpr->common = common; + jfexpr->output = output; + jfexpr->on_empty = jtc->on_empty; + jfexpr->on_error = jtc->on_error; + if (!jfexpr->on_error && errorOnError) + jfexpr->on_error = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); + jfexpr->omit_quotes = jtc->omit_quotes; + jfexpr->wrapper = jtc->wrapper; + jfexpr->location = jtc->location; + + output->typeName = jtc->typeName; + output->returning = makeNode(JsonReturning); + output->returning->format = jtc->format; + + default_format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + + common->pathname = NULL; + /* formatted_expr will be set later in transformJsonValueExpr(). */ + common->expr = makeJsonValueExpr((Expr *) contextItemExpr, NULL, + default_format); + common->passing = passingArgs; + + if (jtc->pathspec) + pathspec = jtc->pathspec; + else + { + /* Construct default path as '$."column_name"' */ + StringInfoData path; + + initStringInfo(&path); + + appendStringInfoString(&path, "$."); + escape_json(&path, jtc->name); + + pathspec = path.data; + } + + common->pathspec = makeStringConst(pathspec, -1); + + return (Node *) jfexpr; +} + +static bool +isJsonTablePathNameDuplicate(JsonTableParseContext *cxt, const char *pathname) +{ + ListCell *lc; + + foreach(lc, cxt->pathNames) + { + if (!strcmp(pathname, (const char *) lfirst(lc))) + return true; + } + + return false; +} + +/* Register the column name in the path name list. */ +static void +registerJsonTableColumn(JsonTableParseContext *cxt, char *colname) +{ + if (isJsonTablePathNameDuplicate(cxt, colname)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_ALIAS), + errmsg("duplicate JSON_TABLE column name: %s", colname), + errhint("JSON_TABLE column names must be distinct from one another."))); + + cxt->pathNames = lappend(cxt->pathNames, colname); +} + +/* Recursively register all nested column names in the path name list. */ +static void +registerAllJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED) + { + if (jtc->pathname) + registerJsonTableColumn(cxt, jtc->pathname); + + registerAllJsonTableColumns(cxt, jtc->columns); + } + else + { + registerJsonTableColumn(cxt, jtc->name); + } + } +} + +/* Generate a new unique JSON_TABLE path name. */ +static char * +generateJsonTablePathName(JsonTableParseContext *cxt) +{ + char namebuf[32]; + char *name = namebuf; + + do + { + snprintf(namebuf, sizeof(namebuf), "json_table_path_%d", + ++cxt->pathNameId); + } while (isJsonTablePathNameDuplicate(cxt, name)); + + name = pstrdup(name); + cxt->pathNames = lappend(cxt->pathNames, name); + + return name; +} + +/* Collect sibling path names from plan to the specified list. */ +static void +collectSiblingPathsInJsonTablePlan(JsonTablePlan *plan, List **paths) +{ + if (plan->plan_type == JSTP_SIMPLE) + *paths = lappend(*paths, plan->pathname); + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + *paths = lappend(*paths, plan->plan1->pathname); + } + else if (plan->join_type == JSTPJ_CROSS || + plan->join_type == JSTPJ_UNION) + { + collectSiblingPathsInJsonTablePlan(plan->plan1, paths); + collectSiblingPathsInJsonTablePlan(plan->plan2, paths); + } + else + elog(ERROR, "invalid JSON_TABLE join type %d", + plan->join_type); + } +} + +/* + * Validate child JSON_TABLE plan by checking that: + * - all nested columns have path names specified + * - all nested columns have corresponding node in the sibling plan + * - plan does not contain duplicate or extra nodes + */ +static void +validateJsonTableChildPlan(ParseState *pstate, JsonTablePlan *plan, + List *columns) +{ + ListCell *lc1; + List *siblings = NIL; + int nchildren = 0; + + if (plan) + collectSiblingPathsInJsonTablePlan(plan, &siblings); + + foreach(lc1, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc1)); + + if (jtc->coltype == JTC_NESTED) + { + ListCell *lc2; + bool found = false; + + if (!jtc->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("nested JSON_TABLE columns must contain an explicit AS pathname specification if an explicit PLAN clause is used"), + parser_errposition(pstate, jtc->location))); + + /* find nested path name in the list of sibling path names */ + foreach(lc2, siblings) + { + if ((found = !strcmp(jtc->pathname, lfirst(lc2)))) + break; + } + + if (!found) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node for nested path %s was not found in plan.", jtc->pathname), + parser_errposition(pstate, jtc->location))); + + nchildren++; + } + } + + if (list_length(siblings) > nchildren) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node contains some extra or duplicate sibling nodes."), + parser_errposition(pstate, plan ? plan->location : -1))); +} + +static JsonTableColumn * +findNestedJsonTableColumn(List *columns, const char *pathname) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED && + jtc->pathname && + !strcmp(jtc->pathname, pathname)) + return jtc; + } + + return NULL; +} + +static Node * +transformNestedJsonTableColumn(JsonTableParseContext *cxt, JsonTableColumn *jtc, + JsonTablePlan *plan) +{ + JsonTableParent *node; + char *pathname = jtc->pathname; + + node = transformJsonTableColumns(cxt, plan, jtc->columns, jtc->pathspec, + &pathname, jtc->location); + + return (Node *) node; +} + +static Node * +makeJsonTableSiblingJoin(bool cross, Node *lnode, Node *rnode) +{ + JsonTableSibling *join = makeNode(JsonTableSibling); + + join->larg = lnode; + join->rarg = rnode; + join->cross = cross; + + return (Node *) join; +} + +/* + * Recursively transform child JSON_TABLE plan. + * + * Default plan is transformed into a cross/union join of its nested columns. + * Simple and outer/inner plans are transformed into a JsonTableParent by + * finding and transforming corresponding nested column. + * Sibling plans are recursively transformed into a JsonTableSibling. + */ +static Node * +transformJsonTableChildPlan(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns) +{ + JsonTableColumn *jtc = NULL; + + if (!plan || plan->plan_type == JSTP_DEFAULT) + { + /* unspecified or default plan */ + Node *res = NULL; + ListCell *lc; + bool cross = plan && (plan->join_type & JSTPJ_CROSS); + + /* transform all nested columns into cross/union join */ + foreach(lc, columns) + { + JsonTableColumn *col = castNode(JsonTableColumn, lfirst(lc)); + Node *node; + + if (col->coltype != JTC_NESTED) + continue; + + node = transformNestedJsonTableColumn(cxt, col, plan); + + /* join transformed node with previous sibling nodes */ + res = res ? makeJsonTableSiblingJoin(cross, res, node) : node; + } + + return res; + } + else if (plan->plan_type == JSTP_SIMPLE) + { + jtc = findNestedJsonTableColumn(columns, plan->pathname); + } + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + jtc = findNestedJsonTableColumn(columns, plan->plan1->pathname); + } + else + { + Node *node1 = transformJsonTableChildPlan(cxt, plan->plan1, + columns); + Node *node2 = transformJsonTableChildPlan(cxt, plan->plan2, + columns); + + return makeJsonTableSiblingJoin(plan->join_type == JSTPJ_CROSS, + node1, node2); + } + } + else + elog(ERROR, "invalid JSON_TABLE plan type %d", plan->plan_type); + + if (!jtc) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name was %s not found in nested columns list.", + plan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + return transformNestedJsonTableColumn(cxt, jtc, plan); +} + +/* Check whether type is json/jsonb, array, or record. */ +static bool +typeIsComposite(Oid typid) +{ + char typtype; + + if (typid == JSONOID || + typid == JSONBOID || + typid == RECORDOID || + type_is_array(typid)) + return true; + + typtype = get_typtype(typid); + + if (typtype == TYPTYPE_COMPOSITE) + return true; + + if (typtype == TYPTYPE_DOMAIN) + return typeIsComposite(getBaseType(typid)); + + return false; +} + +/* Append transformed non-nested JSON_TABLE columns to the TableFunc node */ +static void +appendJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *col; + ParseState *pstate = cxt->pstate; + JsonTable *jt = cxt->table; + TableFunc *tf = cxt->tablefunc; + bool errorOnError = jt->on_error && + jt->on_error->btype == JSON_BEHAVIOR_ERROR; + + foreach(col, columns) + { + JsonTableColumn *rawc = castNode(JsonTableColumn, lfirst(col)); + Oid typid; + int32 typmod; + Node *colexpr; + + if (rawc->name) + { + /* make sure column names are unique */ + ListCell *colname; + + foreach(colname, tf->colnames) + if (!strcmp((const char *) colname, rawc->name)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("column name \"%s\" is not unique", + rawc->name), + parser_errposition(pstate, rawc->location))); + + tf->colnames = lappend(tf->colnames, + makeString(pstrdup(rawc->name))); + } + + /* + * Determine the type and typmod for the new column. FOR ORDINALITY + * columns are INTEGER by standard; the others are user-specified. + */ + switch (rawc->coltype) + { + case JTC_FOR_ORDINALITY: + colexpr = NULL; + typid = INT4OID; + typmod = -1; + break; + + case JTC_REGULAR: + typenameTypeIdAndMod(pstate, rawc->typeName, &typid, &typmod); + + /* + * Use implicit FORMAT JSON for composite types (arrays and + * records) + */ + if (typeIsComposite(typid)) + rawc->coltype = JTC_FORMATTED; + else if (rawc->wrapper != JSW_NONE) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use WITH WRAPPER clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + else if (rawc->omit_quotes) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use OMIT QUOTES clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + + /* FALLTHROUGH */ + case JTC_EXISTS: + case JTC_FORMATTED: + { + Node *je; + CaseTestExpr *param = makeNode(CaseTestExpr); + + param->collation = InvalidOid; + param->typeId = cxt->contextItemTypid; + param->typeMod = -1; + + je = transformJsonTableColumn(rawc, (Node *) param, + NIL, errorOnError); + + colexpr = transformExpr(pstate, je, EXPR_KIND_FROM_FUNCTION); + assign_expr_collations(pstate, colexpr); + + typid = exprType(colexpr); + typmod = exprTypmod(colexpr); + break; + } + + case JTC_NESTED: + continue; + + default: + elog(ERROR, "unknown JSON_TABLE column type: %d", rawc->coltype); + break; + } + + tf->coltypes = lappend_oid(tf->coltypes, typid); + tf->coltypmods = lappend_int(tf->coltypmods, typmod); + tf->colcollations = lappend_oid(tf->colcollations, get_typcollation(typid)); + tf->colvalexprs = lappend(tf->colvalexprs, colexpr); + } +} + +/* + * Create transformed JSON_TABLE parent plan node by appending all non-nested + * columns to the TableFunc node and remembering their indices in the + * colvalexprs list. + */ +static JsonTableParent * +makeParentJsonTableNode(JsonTableParseContext *cxt, char *pathSpec, char *pathName, + List *columns) +{ + JsonTableParent *node = makeNode(JsonTableParent); + + node->path = makeNode(JsonTablePath); + node->path->value = makeConst(JSONPATHOID, -1, InvalidOid, -1, + DirectFunctionCall1(jsonpath_in, + CStringGetDatum(pathSpec)), + false, false); + if (pathName) + node->path->name = pstrdup(pathName); + + /* save start of column range */ + node->colMin = list_length(cxt->tablefunc->colvalexprs); + + appendJsonTableColumns(cxt, columns); + + /* save end of column range */ + node->colMax = list_length(cxt->tablefunc->colvalexprs) - 1; + + node->errorOnError = + cxt->table->on_error && + cxt->table->on_error->btype == JSON_BEHAVIOR_ERROR; + + return node; +} + +static JsonTableParent * +transformJsonTableColumns(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns, char *pathSpec, char **pathName, + int location) +{ + JsonTableParent *node; + JsonTablePlan *childPlan; + bool defaultPlan = !plan || plan->plan_type == JSTP_DEFAULT; + + if (!*pathName) + { + if (cxt->table->plan) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE expression"), + errdetail("JSON_TABLE columns must contain " + "explicit AS pathname specification if " + "explicit PLAN clause is used"), + parser_errposition(cxt->pstate, location))); + + *pathName = generateJsonTablePathName(cxt); + } + + if (defaultPlan) + childPlan = plan; + else + { + /* validate parent and child plans */ + JsonTablePlan *parentPlan; + + if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type != JSTPJ_INNER && + plan->join_type != JSTPJ_OUTER) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Expected INNER or OUTER JSON_TABLE plan node."), + parser_errposition(cxt->pstate, plan->location))); + + parentPlan = plan->plan1; + childPlan = plan->plan2; + + Assert(parentPlan->plan_type != JSTP_JOINED); + Assert(parentPlan->pathname); + } + else + { + parentPlan = plan; + childPlan = NULL; + } + + if (strcmp(parentPlan->pathname, *pathName)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name mismatch: expected %s but %s is given.", + *pathName, parentPlan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + validateJsonTableChildPlan(cxt->pstate, childPlan, columns); + } + + /* transform only non-nested columns */ + node = makeParentJsonTableNode(cxt, pathSpec, *pathName, columns); + + if (childPlan || defaultPlan) + { + /* transform recursively nested columns */ + node->child = transformJsonTableChildPlan(cxt, childPlan, columns); + if (node->child) + node->outerJoin = !plan || (plan->join_type & JSTPJ_OUTER); + /* else: default plan case, no children found */ + } + + return node; +} + +/* + * transformJsonTable - + * Transform a raw JsonTable into TableFunc. + * + * Transform the document-generating expression, the row-generating expression, + * the column-generating expressions, and the default value expressions. + */ +ParseNamespaceItem * +transformJsonTable(ParseState *pstate, JsonTable *jt) +{ + JsonTableParseContext cxt; + TableFunc *tf = makeNode(TableFunc); + JsonFuncExpr *jfe = makeNode(JsonFuncExpr); + JsonExpr *je; + JsonTablePlan *plan = jt->plan; + JsonCommon *jscommon; + char *rootPathName = jt->common->pathname; + char *rootPath; + bool is_lateral; + + cxt.pstate = pstate; + cxt.table = jt; + cxt.tablefunc = tf; + cxt.pathNames = NIL; + cxt.pathNameId = 0; + + if (rootPathName) + registerJsonTableColumn(&cxt, rootPathName); + + registerAllJsonTableColumns(&cxt, jt->columns); + +#if 0 /* XXX it' unclear from the standard whether + * root path name is mandatory or not */ + if (plan && plan->plan_type != JSTP_DEFAULT && !rootPathName) + { + /* Assign root path name and create corresponding plan node */ + JsonTablePlan *rootNode = makeNode(JsonTablePlan); + JsonTablePlan *rootPlan = (JsonTablePlan *) + makeJsonTableJoinedPlan(JSTPJ_OUTER, (Node *) rootNode, + (Node *) plan, jt->location); + + rootPathName = generateJsonTablePathName(&cxt); + + rootNode->plan_type = JSTP_SIMPLE; + rootNode->pathname = rootPathName; + + plan = rootPlan; + } +#endif + + jscommon = copyObject(jt->common); + jscommon->pathspec = makeStringConst(pstrdup("$"), -1); + + jfe->op = JSON_TABLE_OP; + jfe->common = jscommon; + jfe->on_error = jt->on_error; + jfe->location = jt->common->location; + + /* + * We make lateral_only names of this level visible, whether or not the + * RangeTableFunc is explicitly marked LATERAL. This is needed for SQL + * spec compliance and seems useful on convenience grounds for all + * functions in FROM. + * + * (LATERAL can't nest within a single pstate level, so we don't need + * save/restore logic here.) + */ + Assert(!pstate->p_lateral_active); + pstate->p_lateral_active = true; + + tf->functype = TFT_JSON_TABLE; + tf->docexpr = transformExpr(pstate, (Node *) jfe, EXPR_KIND_FROM_FUNCTION); + + cxt.contextItemTypid = exprType(tf->docexpr); + + if (!IsA(jt->common->pathspec, A_Const) || + castNode(A_Const, jt->common->pathspec)->val.node.type != T_String) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("only string constants supported in JSON_TABLE path specification"), + parser_errposition(pstate, + exprLocation(jt->common->pathspec)))); + + rootPath = castNode(A_Const, jt->common->pathspec)->val.sval.sval; + + tf->plan = (Node *) transformJsonTableColumns(&cxt, plan, jt->columns, + rootPath, &rootPathName, + jt->common->location); + + /* Also save a copy of the PASSING arguments in the TableFunc node. */ + je = (JsonExpr *) tf->docexpr; + tf->passingvalexprs = copyObject(je->passing_values); + + tf->ordinalitycol = -1; /* undefine ordinality column number */ + tf->location = jt->location; + + pstate->p_lateral_active = false; + + /* + * Mark the RTE as LATERAL if the user said LATERAL explicitly, or if + * there are any lateral cross-references in it. + */ + is_lateral = jt->lateral || contain_vars_of_level((Node *) tf, 0); + + return addRangeTableEntryForTableFunc(pstate, + tf, jt->alias, is_lateral, true); +} diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 864ea9b0d5..3ac7e113f0 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -2073,7 +2073,8 @@ addRangeTableEntryForTableFunc(ParseState *pstate, Assert(list_length(tf->coltypmods) == list_length(tf->colnames)); Assert(list_length(tf->colcollations) == list_length(tf->colnames)); - refname = alias ? alias->aliasname : pstrdup("xmltable"); + refname = alias ? alias->aliasname : + pstrdup(tf->functype == TFT_XMLTABLE ? "xmltable" : "json_table"); rte->rtekind = RTE_TABLEFUNC; rte->relid = InvalidOid; @@ -2096,7 +2097,7 @@ addRangeTableEntryForTableFunc(ParseState *pstate, ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), errmsg("%s function has %d columns available but %d columns specified", - "XMLTABLE", + tf->functype == TFT_XMLTABLE ? "XMLTABLE" : "JSON_TABLE", list_length(tf->colnames), numaliases))); rte->eref = eref; diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4f94fc69d6..6500e42485 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1992,6 +1992,9 @@ FigureColnameInternal(Node *node, char **name) case JSON_EXISTS_OP: *name = "json_exists"; return 2; + case JSON_TABLE_OP: + *name = "json_table"; + return 2; } break; default: diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index eded22e194..92d76d5cde 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -61,10 +61,12 @@ #include "catalog/pg_collation.h" #include "catalog/pg_type.h" +#include "executor/execExpr.h" #include "funcapi.h" #include "lib/stringinfo.h" #include "miscadmin.h" #include "nodes/miscnodes.h" +#include "nodes/nodeFuncs.h" #include "regex/regex.h" #include "utils/builtins.h" #include "utils/date.h" @@ -75,6 +77,8 @@ #include "utils/guc.h" #include "utils/json.h" #include "utils/jsonpath.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" #include "utils/timestamp.h" #include "utils/varlena.h" @@ -156,6 +160,61 @@ typedef struct JsonValueListIterator ListCell *next; } JsonValueListIterator; +/* Structures for JSON_TABLE execution */ + +typedef enum JsonTablePlanStateType +{ + JSON_TABLE_SCAN_STATE = 0, + JSON_TABLE_JOIN_STATE +} JsonTablePlanStateType; + +typedef struct JsonTablePlanState +{ + JsonTablePlanStateType type; + + struct JsonTablePlanState *parent; + struct JsonTablePlanState *nested; +} JsonTablePlanState; + +typedef struct JsonTableScanState +{ + JsonTablePlanState plan; + + MemoryContext mcxt; + JsonPath *path; + List *args; + JsonValueList found; + JsonValueListIterator iter; + Datum current; + int ordinal; + bool currentIsNull; + bool outerJoin; + bool errorOnError; + bool advanceNested; + bool reset; +} JsonTableScanState; + +typedef struct JsonTableJoinState +{ + JsonTablePlanState plan; + + JsonTablePlanState *left; + JsonTablePlanState *right; + bool cross; + bool advanceRight; +} JsonTableJoinState; + +/* random number to identify JsonTableExecContext */ +#define JSON_TABLE_EXEC_CONTEXT_MAGIC 418352867 + +typedef struct JsonTableExecContext +{ + int magic; + JsonTableScanState **colexprscans; + JsonTableScanState *root; + bool empty; +} JsonTableExecContext; + /* strict/lax flags is decomposed into four [un]wrap/error flags */ #define jspStrictAbsenseOfErrors(cxt) (!(cxt)->laxMode) #define jspAutoUnwrap(cxt) ((cxt)->laxMode) @@ -248,6 +307,7 @@ static JsonPathExecResult getArrayIndex(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbValue *jb, int32 *index); static JsonBaseObjectInfo setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id); +static void JsonValueListClear(JsonValueList *jvl); static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv); static int JsonValueListLength(const JsonValueList *jvl); static bool JsonValueListIsEmpty(JsonValueList *jvl); @@ -265,6 +325,14 @@ static JsonbValue *wrapItemsInArray(const JsonValueList *items); static int compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, bool useTz, bool *cast_error); + +static JsonTablePlanState *JsonTableInitPlanState(JsonTableExecContext *cxt, + Node *plan, + JsonTablePlanState *parent); +static bool JsonTablePlanNextRow(JsonTablePlanState *state); +static bool JsonTableScanNextRow(JsonTableScanState *scan); + + /****************** User interface to JsonPath executor ********************/ /* @@ -2518,6 +2586,13 @@ setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id) return baseObject; } +static void +JsonValueListClear(JsonValueList *jvl) +{ + jvl->singleton = NULL; + jvl->list = NULL; +} + static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv) { @@ -3123,3 +3198,471 @@ JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); } } + +/************************ JSON_TABLE functions ***************************/ + +/* + * Returns private data from executor state. Ensure validity by check with + * MAGIC number. + */ +static inline JsonTableExecContext * +GetJsonTableExecContext(TableFuncScanState *state, const char *fname) +{ + JsonTableExecContext *result; + + if (!IsA(state, TableFuncScanState)) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + result = (JsonTableExecContext *) state->opaque; + if (result->magic != JSON_TABLE_EXEC_CONTEXT_MAGIC) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + + return result; +} + +/* Recursively initialize JSON_TABLE scan / join state */ +static JsonTableJoinState * +JsonTableInitJoinState(JsonTableExecContext *cxt, JsonTableSibling *plan, + JsonTablePlanState *parent) +{ + JsonTableJoinState *join = palloc0(sizeof(*join)); + + join->plan.type = JSON_TABLE_JOIN_STATE; + /* parent and nested not set. */ + + join->cross = plan->cross; + join->left = JsonTableInitPlanState(cxt, plan->larg, parent); + join->right = JsonTableInitPlanState(cxt, plan->rarg, parent); + + return join; +} + +static JsonTableScanState * +JsonTableInitScanState(JsonTableExecContext *cxt, JsonTableParent *plan, + JsonTablePlanState *parent, + List *args, MemoryContext mcxt) +{ + JsonTableScanState *scan = palloc0(sizeof(*scan)); + int i; + + scan->plan.type = JSON_TABLE_SCAN_STATE; + scan->plan.parent = parent; + + scan->outerJoin = plan->outerJoin; + scan->errorOnError = plan->errorOnError; + scan->path = DatumGetJsonPathP(plan->path->value->constvalue); + scan->args = args; + scan->mcxt = AllocSetContextCreate(mcxt, "JsonTableExecContext", + ALLOCSET_DEFAULT_SIZES); + + /* + * Set after settings scan->args and scan->mcxt, because the recursive call + * wants to use those values. + */ + scan->plan.nested = plan->child ? + JsonTableInitPlanState(cxt, plan->child, (JsonTablePlanState *) scan) : + NULL; + + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + + for (i = plan->colMin; i <= plan->colMax; i++) + cxt->colexprscans[i] = scan; + + return scan; +} + +/* Recursively initialize JSON_TABLE scan state */ +static JsonTablePlanState * +JsonTableInitPlanState(JsonTableExecContext *cxt, Node *plan, + JsonTablePlanState *parent) +{ + JsonTablePlanState *state; + + if (IsA(plan, JsonTableSibling)) + { + JsonTableSibling *join = castNode(JsonTableSibling, plan); + + state = (JsonTablePlanState *) + JsonTableInitJoinState(cxt, join, parent); + } + else + { + JsonTableParent *scan = castNode(JsonTableParent, plan); + JsonTableScanState *parent_scan = (JsonTableScanState *) parent; + + Assert(parent_scan); + state = (JsonTablePlanState *) + JsonTableInitScanState(cxt, scan, parent, parent_scan->args, + parent_scan->mcxt); + } + + return state; +} + +/* + * JsonTableInitOpaque + * Fill in TableFuncScanState->opaque for JsonTable processor + */ +static void +JsonTableInitOpaque(TableFuncScanState *state, int natts) +{ + JsonTableExecContext *cxt; + PlanState *ps = &state->ss.ps; + TableFuncScan *tfs = castNode(TableFuncScan, ps->plan); + TableFunc *tf = tfs->tablefunc; + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + JsonExpr *je = castNode(JsonExpr, tf->docexpr); + List *args = NIL; + + cxt = palloc0(sizeof(JsonTableExecContext)); + cxt->magic = JSON_TABLE_EXEC_CONTEXT_MAGIC; + + if (state->passingvalexprs) + { + ListCell *exprlc; + ListCell *namelc; + + Assert(list_length(state->passingvalexprs) == + list_length(je->passing_names)); + forboth(exprlc, state->passingvalexprs, + namelc, je->passing_names) + { + ExprState *state = lfirst_node(ExprState, exprlc); + String *name = lfirst_node(String, namelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(name->sval); + var->typid = exprType((Node *) state->expr); + var->typmod = exprTypmod((Node *) state->expr); + + /* + * Evaluate the expression and save the value to be returned by + * GetJsonPathVar(). + */ + var->value = ExecEvalExpr(state, ps->ps_ExprContext, + &var->isnull); + + args = lappend(args, var); + } + } + + cxt->colexprscans = palloc(sizeof(JsonTableScanState *) * + list_length(tf->colvalexprs)); + + cxt->root = JsonTableInitScanState(cxt, root, NULL, args, + CurrentMemoryContext); + state->opaque = cxt; +} + +/* Reset scan iterator to the beginning of the item list */ +static void +JsonTableRescan(JsonTableScanState *scan) +{ + JsonValueListInitIterator(&scan->found, &scan->iter); + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + scan->advanceNested = false; + scan->ordinal = 0; +} + +/* Reset context item of a scan, execute JSON path and reset a scan */ +static void +JsonTableResetContextItem(JsonTableScanState *scan, Datum item) +{ + MemoryContext oldcxt; + JsonPathExecResult res; + Jsonb *js = (Jsonb *) DatumGetJsonbP(item); + + JsonValueListClear(&scan->found); + + MemoryContextResetOnly(scan->mcxt); + + oldcxt = MemoryContextSwitchTo(scan->mcxt); + + res = executeJsonPath(scan->path, scan->args, GetJsonPathVar, js, + scan->errorOnError, &scan->found, + false /* FIXME */ ); + + MemoryContextSwitchTo(oldcxt); + + if (jperIsError(res)) + { + Assert(!scan->errorOnError); + JsonValueListClear(&scan->found); /* EMPTY ON ERROR case */ + } + + JsonTableRescan(scan); +} + +/* + * JsonTableSetDocument + * Install the input document + */ +static void +JsonTableSetDocument(TableFuncScanState *state, Datum value) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableSetDocument"); + + JsonTableResetContextItem(cxt->root, value); +} + +/* Recursively reset scan and its child nodes */ +static void +JsonTableRescanRecursive(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTableRescanRecursive(join->left); + JsonTableRescanRecursive(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan = (JsonTableScanState *) state; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + JsonTableRescan(scan); + if (scan->plan.nested) + JsonTableRescanRecursive(scan->plan.nested); + } +} + +/* + * Fetch next row from a cross/union joined scan. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTablePlanNextRow(JsonTablePlanState *state) +{ + JsonTableJoinState *join; + + if (state->type == JSON_TABLE_SCAN_STATE) + return JsonTableScanNextRow((JsonTableScanState *) state); + + join = (JsonTableJoinState *) state; + if (join->advanceRight) + { + /* fetch next inner row */ + if (JsonTablePlanNextRow(join->right)) + return true; + + /* inner rows are exhausted */ + if (join->cross) + join->advanceRight = false; /* next outer row */ + else + return false; /* end of scan */ + } + + while (!join->advanceRight) + { + /* fetch next outer row */ + bool left = JsonTablePlanNextRow(join->left); + + if (join->cross) + { + if (!left) + return false; /* end of scan */ + + JsonTableRescanRecursive(join->right); + + if (!JsonTablePlanNextRow(join->right)) + continue; /* next outer row */ + + join->advanceRight = true; /* next inner row */ + } + else if (!left) + { + if (!JsonTablePlanNextRow(join->right)) + return false; /* end of scan */ + + join->advanceRight = true; /* next inner row */ + } + + break; + } + + return true; +} + +/* Recursively set 'reset' flag of scan and its child nodes */ +static void +JsonTablePlanReset(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTablePlanReset(join->left); + JsonTablePlanReset(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + scan = (JsonTableScanState *) state; + scan->reset = true; + scan->advanceNested = false; + + if (scan->plan.nested) + JsonTablePlanReset(scan->plan.nested); + } +} + +/* + * Fetch next row from a simple scan with outer/inner joined nested subscans. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTableScanNextRow(JsonTableScanState *scan) +{ + /* reset context item if requested */ + if (scan->reset) + { + JsonTableScanState *parent_scan = + (JsonTableScanState *) scan->plan.parent; + + Assert(parent_scan && !parent_scan->currentIsNull); + JsonTableResetContextItem(scan, parent_scan->current); + scan->reset = false; + } + + if (scan->advanceNested) + { + /* fetch next nested row */ + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested) + return true; + } + + for (;;) + { + /* fetch next row */ + JsonbValue *jbv = JsonValueListNext(&scan->found, &scan->iter); + MemoryContext oldcxt; + + if (!jbv) + { + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + return false; /* end of scan */ + } + + /* set current row item */ + oldcxt = MemoryContextSwitchTo(scan->mcxt); + scan->current = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + scan->currentIsNull = false; + MemoryContextSwitchTo(oldcxt); + + scan->ordinal++; + + if (!scan->plan.nested) + break; + + JsonTablePlanReset(scan->plan.nested); + + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested || scan->outerJoin) + break; + } + + return true; +} + +/* + * JsonTableFetchRow + * Prepare the next "current" tuple for upcoming GetValue calls. + * Returns FALSE if the row-filter expression returned no more rows. + */ +static bool +JsonTableFetchRow(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableFetchRow"); + + if (cxt->empty) + return false; + + return JsonTableScanNextRow(cxt->root); +} + +/* + * JsonTableGetValue + * Return the value for column number 'colnum' for the current row. + * + * This leaks memory, so be sure to reset often the context in which it's + * called. + */ +static Datum +JsonTableGetValue(TableFuncScanState *state, int colnum, + Oid typid, int32 typmod, bool *isnull) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableGetValue"); + ExprContext *econtext = state->ss.ps.ps_ExprContext; + ExprState *estate = list_nth(state->colvalexprs, colnum); + JsonTableScanState *scan = cxt->colexprscans[colnum]; + Datum result; + + if (scan->currentIsNull) /* NULL from outer/union join */ + { + result = (Datum) 0; + *isnull = true; + } + else if (estate) /* regular column */ + { + Datum saved_caseValue = econtext->caseValue_datum; + bool saved_caseIsNull = econtext->caseValue_isNull; + + /* Pass the value for CaseTestExpr that may be present in colexpr */ + econtext->caseValue_datum = scan->current; + econtext->caseValue_isNull = false; + + result = ExecEvalExpr(estate, econtext, isnull); + + econtext->caseValue_datum = saved_caseValue; + econtext->caseValue_isNull = saved_caseIsNull; + } + else + { + result = Int32GetDatum(scan->ordinal); /* ordinality column */ + *isnull = false; + } + + return result; +} + +/* + * JsonTableDestroyOpaque + */ +static void +JsonTableDestroyOpaque(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableDestroyOpaque"); + + /* not valid anymore */ + cxt->magic = 0; + + state->opaque = NULL; +} + +const TableFuncRoutine JsonbTableRoutine = +{ + JsonTableInitOpaque, + JsonTableSetDocument, + NULL, + NULL, + NULL, + JsonTableFetchRow, + JsonTableGetValue, + JsonTableDestroyOpaque +}; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index a23e1f7af5..87c0aa27b5 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -522,6 +522,8 @@ static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); static void get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit); +static void get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8606,7 +8608,8 @@ get_json_behavior(JsonBehavior *behavior, deparse_context *context, /* * get_json_expr_options * - * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS and + * JSON_TABLE columns. */ static void get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, @@ -9854,6 +9857,9 @@ get_rule_expr(Node *node, deparse_context *context, case JSON_EXISTS_OP: appendStringInfoString(buf, "JSON_EXISTS("); break; + default: + elog(ERROR, "unexpected JsonExpr type: %d", jexpr->op); + break; } get_rule_expr(jexpr->formatted_expr, context, showimplicit); @@ -11207,16 +11213,14 @@ get_sublink_expr(SubLink *sublink, deparse_context *context) /* ---------- - * get_tablefunc - Parse back a table function + * get_xmltable - Parse back a XMLTABLE function * ---------- */ static void -get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +get_xmltable(TableFunc *tf, deparse_context *context, bool showimplicit) { StringInfo buf = context->buf; - /* XMLTABLE is the only existing implementation. */ - appendStringInfoString(buf, "XMLTABLE("); if (tf->ns_uris != NIL) @@ -11307,6 +11311,271 @@ get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) appendStringInfoChar(buf, ')'); } +/* + * get_json_nested_columns - Parse back nested JSON_TABLE columns + */ +static void +get_json_table_nested_columns(TableFunc *tf, Node *node, + deparse_context *context, bool showimplicit, + bool needcomma) +{ + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_nested_columns(tf, n->larg, context, showimplicit, + needcomma); + get_json_table_nested_columns(tf, n->rarg, context, showimplicit, true); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + if (needcomma) + appendStringInfoChar(context->buf, ','); + + appendStringInfoChar(context->buf, ' '); + appendContextKeyword(context, "NESTED PATH ", 0, 0, 0); + get_const_expr(n->path->value, context, -1); + appendStringInfo(context->buf, " AS %s", quote_identifier(n->path->name)); + get_json_table_columns(tf, n, context, showimplicit); + } +} + +/* + * get_json_table_plan - Parse back a JSON_TABLE plan + */ +static void +get_json_table_plan(TableFunc *tf, Node *node, deparse_context *context, + bool parenthesize) +{ + if (parenthesize) + appendStringInfoChar(context->buf, '('); + + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_plan(tf, n->larg, context, + IsA(n->larg, JsonTableSibling) || + castNode(JsonTableParent, n->larg)->child); + + appendStringInfoString(context->buf, n->cross ? " CROSS " : " UNION "); + + get_json_table_plan(tf, n->rarg, context, + IsA(n->rarg, JsonTableSibling) || + castNode(JsonTableParent, n->rarg)->child); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + appendStringInfoString(context->buf, quote_identifier(n->path->name)); + + if (n->child) + { + appendStringInfoString(context->buf, + n->outerJoin ? " OUTER " : " INNER "); + get_json_table_plan(tf, n->child, context, + IsA(n->child, JsonTableSibling)); + } + } + + if (parenthesize) + appendStringInfoChar(context->buf, ')'); +} + +/* + * get_json_table_columns - Parse back JSON_TABLE columns + */ +static void +get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + ListCell *lc_colname; + ListCell *lc_coltype; + ListCell *lc_coltypmod; + ListCell *lc_colvalexpr; + int colnum = 0; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "COLUMNS (", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forfour(lc_colname, tf->colnames, + lc_coltype, tf->coltypes, + lc_coltypmod, tf->coltypmods, + lc_colvalexpr, tf->colvalexprs) + { + char *colname = strVal(lfirst(lc_colname)); + JsonExpr *colexpr; + Oid typid; + int32 typmod; + bool ordinality; + JsonBehaviorType default_behavior; + + typid = lfirst_oid(lc_coltype); + typmod = lfirst_int(lc_coltypmod); + colexpr = castNode(JsonExpr, lfirst(lc_colvalexpr)); + + if (colnum < node->colMin) + { + colnum++; + continue; + } + + if (colnum > node->colMax) + break; + + if (colnum > node->colMin) + appendStringInfoString(buf, ", "); + + colnum++; + + ordinality = !colexpr; + + appendContextKeyword(context, "", 0, 0, 0); + + appendStringInfo(buf, "%s %s", quote_identifier(colname), + ordinality ? "FOR ORDINALITY" : + format_type_with_typemod(typid, typmod)); + if (ordinality) + continue; + + if (colexpr->op == JSON_EXISTS_OP) + { + appendStringInfoString(buf, " EXISTS"); + default_behavior = JSON_BEHAVIOR_FALSE; + } + else + { + if (colexpr->op == JSON_QUERY_OP) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(typid, &typcategory, &typispreferred); + + if (typcategory == TYPCATEGORY_STRING) + appendStringInfoString(buf, + colexpr->format->format_type == JS_FORMAT_JSONB ? + " FORMAT JSONB" : " FORMAT JSON"); + } + + default_behavior = JSON_BEHAVIOR_NULL; + } + + if (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR) + default_behavior = JSON_BEHAVIOR_ERROR; + + appendStringInfoString(buf, " PATH "); + + get_json_path_spec(colexpr->path_spec, context, showimplicit); + + get_json_expr_options(colexpr, context, default_behavior); + } + + if (node->child) + get_json_table_nested_columns(tf, node->child, context, showimplicit, + node->colMax >= node->colMin); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_json_table - Parse back a JSON_TABLE function + * ---------- + */ +static void +get_json_table(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + + appendStringInfoString(buf, "JSON_TABLE("); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_const_expr(root->path->value, context, -1); + + appendStringInfo(buf, " AS %s", quote_identifier(root->path->name)); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PASSING ", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr((Node *) lfirst(lc2), context, false); + appendStringInfo(buf, " AS %s", + quote_identifier((lfirst_node(String, lc1))->sval) + ); + } + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + } + + get_json_table_columns(tf, root, context, showimplicit); + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PLAN ", 0, 0, 0); + get_json_table_plan(tf, (Node *) root, context, true); + + if (jexpr->on_error->btype != JSON_BEHAVIOR_EMPTY) + get_json_behavior(jexpr->on_error, context, "ERROR"); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_tablefunc - Parse back a table function + * ---------- + */ +static void +get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + /* XMLTABLE and JSON_TABLE are the only existing implementations. */ + + if (tf->functype == TFT_XMLTABLE) + get_xmltable(tf, context, showimplicit); + else if (tf->functype == TFT_JSON_TABLE) + get_json_table(tf, context, showimplicit); +} + /* ---------- * get_from_clause - Parse back a FROM clause * diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 584bf7001a..91453681e3 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1879,6 +1879,8 @@ typedef struct TableFuncScanState ExprState *rowexpr; /* state for row-generating expression */ List *colexprs; /* state for column-generating expression */ List *coldefexprs; /* state for column default expressions */ + List *colvalexprs; /* state for column value expression */ + List *passingvalexprs; /* state for PASSING argument expression */ List *ns_names; /* same as TableFunc.ns_names */ List *ns_uris; /* list of states of namespace URI exprs */ Bitmapset *notnulls; /* nullability flag for each output column */ diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index c1d437b44e..076b1ddab3 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -113,6 +113,8 @@ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, extern JsonValueExpr *makeJsonValueExpr(Expr *raw_expr, Expr *formatted_expr, JsonFormat *format); extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); +extern Node *makeJsonTableJoinedPlan(JsonTablePlanJoinType type, + Node *plan1, Node *plan2, int location); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 7d63a37d5f..b15f71cc92 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1733,6 +1733,19 @@ typedef enum JsonQuotes */ typedef char *JsonPathSpec; +/* + * JsonTableColumnType - + * enumeration of JSON_TABLE column types + */ +typedef enum JsonTableColumnType +{ + JTC_FOR_ORDINALITY, + JTC_REGULAR, + JTC_EXISTS, + JTC_FORMATTED, + JTC_NESTED, +} JsonTableColumnType; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1786,6 +1799,83 @@ typedef struct JsonFuncExpr int location; /* token location, or -1 if unknown */ } JsonFuncExpr; +/* + * JsonTableColumn - + * untransformed representation of JSON_TABLE column + */ +typedef struct JsonTableColumn +{ + NodeTag type; + JsonTableColumnType coltype; /* column type */ + char *name; /* column name */ + TypeName *typeName; /* column type name */ + char *pathspec; /* path specification, if any */ + char *pathname; /* path name, if any */ + JsonFormat *format; /* JSON format clause, if specified */ + JsonWrapper wrapper; /* WRAPPER behavior for formatted columns */ + bool omit_quotes; /* omit or keep quotes on scalar strings? */ + List *columns; /* nested columns */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + int location; /* token location, or -1 if unknown */ +} JsonTableColumn; + +/* + * JsonTablePlanType - + * flags for JSON_TABLE plan node types representation + */ +typedef enum JsonTablePlanType +{ + JSTP_DEFAULT, + JSTP_SIMPLE, + JSTP_JOINED, +} JsonTablePlanType; + +/* + * JsonTablePlanJoinType - + * flags for JSON_TABLE join types representation + */ +typedef enum JsonTablePlanJoinType +{ + JSTPJ_INNER = 0x01, + JSTPJ_OUTER = 0x02, + JSTPJ_CROSS = 0x04, + JSTPJ_UNION = 0x08, +} JsonTablePlanJoinType; + +typedef struct JsonTablePlan JsonTablePlan; + +/* + * JsonTablePlan - + * untransformed representation of JSON_TABLE plan node + */ +struct JsonTablePlan +{ + NodeTag type; + JsonTablePlanType plan_type; /* plan type */ + JsonTablePlanJoinType join_type; /* join type (for joined plan only) */ + JsonTablePlan *plan1; /* first joined plan */ + JsonTablePlan *plan2; /* second joined plan */ + char *pathname; /* path name (for simple plan only) */ + int location; /* token location, or -1 if unknown */ +}; + +/* + * JsonTable - + * untransformed representation of JSON_TABLE + */ +typedef struct JsonTable +{ + NodeTag type; + JsonCommon *common; /* common JSON path syntax fields */ + List *columns; /* list of JsonTableColumn */ + JsonTablePlan *plan; /* join plan, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + Alias *alias; /* table alias in FROM clause */ + bool lateral; /* does it have LATERAL prefix? */ + int location; /* token location, or -1 if unknown */ +} JsonTable; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 64fdbc57fa..bae1de3554 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -86,8 +86,14 @@ typedef struct RangeVar int location; } RangeVar; +typedef enum TableFuncType +{ + TFT_XMLTABLE, + TFT_JSON_TABLE +} TableFuncType; + /* - * TableFunc - node for a table function, such as XMLTABLE. + * TableFunc - node for a table function, such as XMLTABLE or JSON_TABLE. * * Entries in the ns_names list are either String nodes containing * literal namespace names, or NULL pointers to represent DEFAULT. @@ -95,6 +101,8 @@ typedef struct RangeVar typedef struct TableFunc { NodeTag type; + /* XMLTABLE or JSON_TABLE */ + TableFuncType functype; /* list of namespace URI expressions */ List *ns_uris pg_node_attr(query_jumble_ignore); /* list of namespace names or NULL */ @@ -115,8 +123,14 @@ typedef struct TableFunc List *colexprs; /* list of column default expressions */ List *coldefexprs pg_node_attr(query_jumble_ignore); + /* list of column value expressions */ + List *colvalexprs pg_node_attr(query_jumble_ignore); + /* list of PASSING argument expressions */ + List *passingvalexprs pg_node_attr(query_jumble_ignore); /* nullability flag for each output column */ Bitmapset *notnulls pg_node_attr(query_jumble_ignore); + /* JSON_TABLE plan */ + Node *plan pg_node_attr(query_jumble_ignore); /* counts from 0; -1 if none specified */ int ordinalitycol pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ @@ -1552,7 +1566,8 @@ typedef enum JsonExprOp { JSON_VALUE_OP, /* JSON_VALUE() */ JSON_QUERY_OP, /* JSON_QUERY() */ - JSON_EXISTS_OP /* JSON_EXISTS() */ + JSON_EXISTS_OP, /* JSON_EXISTS() */ + JSON_TABLE_OP /* JSON_TABLE() */ } JsonExprOp; /* @@ -1771,6 +1786,48 @@ typedef struct JsonExpr int location; /* token location, or -1 if unknown */ } JsonExpr; +/* + * JsonTablePath + * A JSON path expression to be computed as part of evaluating + * a JSON_TABLE plan node + */ +typedef struct JsonTablePath +{ + NodeTag type; + + Const *value; + char *name; +} JsonTablePath; + +/* + * JsonTableParent - + * transformed representation of parent JSON_TABLE plan node + */ +typedef struct JsonTableParent +{ + NodeTag type; + JsonTablePath *path; + Node *child; /* nested columns, if any */ + bool outerJoin; /* outer or inner join for nested columns? */ + int colMin; /* min column index in the resulting column + * list */ + int colMax; /* max column index in the resulting column + * list */ + bool errorOnError; /* ERROR/EMPTY ON ERROR behavior */ +} JsonTableParent; + +/* + * JsonTableSibling - + * transformed representation of joined sibling JSON_TABLE plan node + */ +typedef struct JsonTableSibling +{ + NodeTag type; + Node *larg; /* left join node */ + Node *rarg; /* right join node */ + bool cross; /* cross or union join? */ +} JsonTableSibling; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0954d9fc7b..f88a6c9ac6 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -242,6 +242,7 @@ PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_table", JSON_TABLE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) @@ -284,6 +285,7 @@ PG_KEYWORD("names", NAMES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("national", NATIONAL, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("natural", NATURAL, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("nchar", NCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("nested", NESTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("new", NEW, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("next", NEXT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("nfc", NFC, UNRESERVED_KEYWORD, BARE_LABEL) @@ -334,7 +336,9 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h index 8e26482db4..7152a8ef48 100644 --- a/src/include/parser/parse_clause.h +++ b/src/include/parser/parse_clause.h @@ -51,4 +51,7 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle, extern Index assignSortGroupRef(TargetEntry *tle, List *tlist); extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList); +/* functions in parse_jsontable.c */ +extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt); + #endif /* PARSE_CLAUSE_H */ diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index 4dae78a98c..b7ada77cb1 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -15,6 +15,7 @@ #define JSONPATH_H #include "fmgr.h" +#include "executor/tablefunc.h" #include "nodes/pg_list.h" #include "nodes/primnodes.h" #include "utils/jsonb.h" @@ -288,4 +289,6 @@ extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars); +extern PGDLLIMPORT const TableFuncRoutine JsonbTableRoutine; + #endif diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out index 8b87580752..995f267404 100644 --- a/src/test/regress/expected/json_sqljson.out +++ b/src/test/regress/expected/json_sqljson.out @@ -16,3 +16,9 @@ ERROR: JSON_QUERY() is not yet implemented for the json type LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); ^ HINT: Try casting the argument to jsonb +-- JSON_TABLE +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); +ERROR: JSON_TABLE() is not yet implemented for the json type +LINE 1: SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo ... + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out index 22f8679917..4323ed6b35 100644 --- a/src/test/regress/expected/jsonb_sqljson.out +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -1040,3 +1040,1072 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.dateti ERROR: functions in index expression must be marked IMMUTABLE CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; +-- JSON_TABLE +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); +ERROR: syntax error at or near "(" +LINE 1: SELECT JSON_TABLE('[]', '$'); + ^ +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); +ERROR: syntax error at or near ")" +LINE 1: SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + ^ +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); +ERROR: JSON_TABLE function has 1 columns available but 2 columns specified +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + foo +----- +(0 rows) + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + item | foo +------+----- + 123 | +(1 row) + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + js | id | id2 | int | text | char(4) | bool | numeric | domain | js | jb | jst | jsc | jsv | jsb | jsbq | aaa | aaa1 | exists1 | exists2 | exists3 | exists4 | js2 | jsb2w | jsb2q | ia | ta | jba +---------------------------------------------------------------------------------------+----+-----+-----+---------+---------+------+---------+---------+--------------+--------------+--------------+------+------+--------------+--------------+-----+------+---------+---------+---------+---------+--------------+----------------+--------------+----+----+----- + 1 | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [] | | | | | | | | | | | | | | | | | | | | | | | | | | | + {} | 1 | 1 | | | | | | | {} | {} | {} | {} | {} | {} | {} | | | f | 0 | | false | {} | [{}] | {} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 2 | 2 | 1 | 1.23 | 1.23 | | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | | | f | 0 | | false | 1.23 | [1.23] | 1.23 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 3 | 3 | 2 | 2 | 2 | | 2 | 2 | "2" | "2" | "2" | "2" | "2" | "2" | 2 | | | f | 0 | | false | "2" | ["2"] | 2 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 4 | 4 | | aaaaaaa | aaaa | | | aaaaaaa | "aaaaaaa" | "aaaaaaa" | "aaaaaaa" | "aaa | "aaa | "aaaaaaa" | | | | f | 0 | | false | "aaaaaaa" | ["aaaaaaa"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 5 | 5 | | foo | foo | | | | "foo" | "foo" | "foo" | "foo | "foo | "foo" | | | | f | 0 | | false | "foo" | ["foo"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 6 | 6 | | | | | | | null | null | null | null | null | null | null | | | f | 0 | | false | null | [null] | null | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 7 | 7 | 0 | false | fals | f | | false | false | false | false | fals | fals | false | false | | | f | 0 | | false | false | [false] | false | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 8 | 8 | 1 | true | true | t | | true | true | true | true | true | true | true | true | | | f | 0 | | false | true | [true] | true | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 9 | 9 | | | | | | | {"aaa": 123} | {"aaa": 123} | {"aaa": 123} | {"aa | {"aa | {"aaa": 123} | {"aaa": 123} | 123 | 123 | t | 1 | 1 | true | {"aaa": 123} | [{"aaa": 123}] | {"aaa": 123} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 10 | 10 | | [1,2] | [1,2 | | | [1,2] | "[1,2]" | "[1,2]" | "[1,2]" | "[1, | "[1, | "[1,2]" | [1, 2] | | | f | 0 | | false | "[1,2]" | ["[1,2]"] | [1, 2] | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 11 | 11 | | "str" | "str | | | "str" | "\"str\"" | "\"str\"" | "\"str\"" | "\"s | "\"s | "\"str\"" | "str" | | | f | 0 | | false | "\"str\"" | ["\"str\""] | "str" | | | +(14 rows) + +-- JSON_TABLE: Test backward parsing +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); +\sv jsonb_table_view +CREATE OR REPLACE VIEW public.jsonb_table_view AS + SELECT id, + id2, + "int", + text, + "char(4)", + bool, + "numeric", + domain, + js, + jb, + jst, + jsc, + jsv, + jsb, + jsbq, + aaa, + aaa1, + exists1, + exists2, + exists3, + js2, + jsb2w, + jsb2q, + ia, + ta, + jba, + a1, + b1, + a11, + a21, + a22 + FROM JSON_TABLE( + 'null'::jsonb, '$[*]' AS json_table_path_1 + PASSING + 1 + 2 AS a, + '"foo"'::json AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, + "int" integer PATH '$', + text text PATH '$', + "char(4)" character(4) PATH '$', + bool boolean PATH '$', + "numeric" numeric PATH '$', + domain jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc character(4) FORMAT JSON PATH '$', + jsv character varying(4) FORMAT JSON PATH '$', + jsb jsonb PATH '$', + jsbq jsonb PATH '$' OMIT QUOTES, + aaa integer PATH '$."aaa"', + aaa1 integer PATH '$."aaa"', + exists1 boolean EXISTS PATH '$."aaa"', + exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia integer[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 + COLUMNS ( + a1 integer PATH '$."a1"', + b1 text PATH '$."b1"', + NESTED PATH '$[*]' AS "p1 1" + COLUMNS ( + a11 text PATH '$."a11"' + ) + ), + NESTED PATH '$[2]' AS p2 + COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" + COLUMNS ( + a21 text PATH '$."a21"' + ), + NESTED PATH '$[*]' AS p22 + COLUMNS ( + a22 text PATH '$."a22"' + ) + ) + ) + PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22)))) + ) +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Table Function Scan on "json_table" + Output: "json_table".id, "json_table".id2, "json_table"."int", "json_table".text, "json_table"."char(4)", "json_table".bool, "json_table"."numeric", "json_table".domain, "json_table".js, "json_table".jb, "json_table".jst, "json_table".jsc, "json_table".jsv, "json_table".jsb, "json_table".jsbq, "json_table".aaa, "json_table".aaa1, "json_table".exists1, "json_table".exists2, "json_table".exists3, "json_table".js2, "json_table".jsb2w, "json_table".jsb2q, "json_table".ia, "json_table".ta, "json_table".jba, "json_table".a1, "json_table".b1, "json_table".a11, "json_table".a21, "json_table".a22 + Table Function Call: JSON_TABLE('null'::jsonb, '$[*]' AS json_table_path_1 PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (id FOR ORDINALITY, id2 FOR ORDINALITY, "int" integer PATH '$', text text PATH '$', "char(4)" character(4) PATH '$', bool boolean PATH '$', "numeric" numeric PATH '$', domain jsonb_test_domain PATH '$', js json PATH '$', jb jsonb PATH '$', jst text FORMAT JSON PATH '$', jsc character(4) FORMAT JSON PATH '$', jsv character varying(4) FORMAT JSON PATH '$', jsb jsonb PATH '$', jsbq jsonb PATH '$' OMIT QUOTES, aaa integer PATH '$."aaa"', aaa1 integer PATH '$."aaa"', exists1 boolean EXISTS PATH '$."aaa"', exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, js2 json PATH '$', jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, jsb2q jsonb PATH '$' OMIT QUOTES, ia integer[] PATH '$', ta text[] PATH '$', jba jsonb[] PATH '$', NESTED PATH '$[1]' AS p1 COLUMNS (a1 integer PATH '$."a1"', b1 text PATH '$."b1"', NESTED PATH '$[*]' AS "p1 1" COLUMNS (a11 text PATH '$."a11"')), NESTED PATH '$[2]' AS p2 COLUMNS ( NESTED PATH '$[*]' AS "p2:1" COLUMNS (a21 text PATH '$."a21"'), NESTED PATH '$[*]' AS p22 COLUMNS (a22 text PATH '$."a22"'))) PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22))))) +(3 rows) + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + js | a +-------+--- + 1 | 1 + "err" | +(2 rows) + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; + a +--- + +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: jsonpath member accessor can only be applied to an object +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: no SQL/JSON item +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 1 +(1 row) + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); + a +--- + 0 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to smallint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to bigint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to real +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 E... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); + a +----- + fal +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to json +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to jsonb +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EX... + ^ +-- JSON_TABLE: nested paths and plans +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb '[]', '$' -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 4: NESTED PATH '$' COLUMNS ( -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: b +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +-- JSON_TABLE: plan validation +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p1) + ^ +DETAIL: Path name mismatch: expected p0 but p1 is given. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 UNION p1 UNION p11) + ^ +DETAIL: Expected INNER or OUTER JSON_TABLE plan node. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 8: NESTED PATH '$' AS p2 COLUMNS ( + ^ +DETAIL: Plan node for nested path p2 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 5: NESTED PATH '$' AS p11 COLUMNS ( foo int ), + ^ +DETAIL: Plan node for nested path p11 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) + ^ +DETAIL: Plan node contains some extra or duplicate sibling nodes. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 6: NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ^ +DETAIL: Plan node for nested path p12 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 9: NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ^ +DETAIL: Plan node for nested path p21 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + bar | foo | baz +-----+-----+----- +(0 rows) + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb 'null', 'strict $[*]' -- without root path name + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- JSON_TABLE: plan execution +CREATE TEMP TABLE jsonb_table_test (js jsonb); +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + n | a | c | b +---+----+----+--- + 1 | 1 | | + 2 | 2 | 10 | + 2 | 2 | | + 2 | 2 | 20 | + 2 | 2 | | 1 + 2 | 2 | | 2 + 2 | 2 | | 3 + 3 | 3 | | 1 + 3 | 3 | | 2 + 4 | -1 | | 1 + 4 | -1 | | 2 +(11 rows) + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + n | a | b | b1 | c | c1 | b +---+---+--------------+-----+------+----+----- + 1 | 1 | [1, 10] | 1 | 1 | | 101 + 1 | 1 | [1, 10] | 1 | null | | 101 + 1 | 1 | [1, 10] | 1 | 2 | | 101 + 1 | 1 | [1, 10] | 10 | 1 | | 110 + 1 | 1 | [1, 10] | 10 | null | | 110 + 1 | 1 | [1, 10] | 10 | 2 | | 110 + 1 | 1 | [2] | 2 | 1 | | 102 + 1 | 1 | [2] | 2 | null | | 102 + 1 | 1 | [2] | 2 | 2 | | 102 + 1 | 1 | [3, 30, 300] | 3 | 1 | | 103 + 1 | 1 | [3, 30, 300] | 3 | null | | 103 + 1 | 1 | [3, 30, 300] | 3 | 2 | | 103 + 1 | 1 | [3, 30, 300] | 30 | 1 | | 130 + 1 | 1 | [3, 30, 300] | 30 | null | | 130 + 1 | 1 | [3, 30, 300] | 30 | 2 | | 130 + 1 | 1 | [3, 30, 300] | 300 | 1 | | 400 + 1 | 1 | [3, 30, 300] | 300 | null | | 400 + 1 | 1 | [3, 30, 300] | 300 | 2 | | 400 + 2 | 2 | | | | | + 3 | | | | | | +(20 rows) + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + x | y | y | z +---+---+--------------+--- + 2 | 1 | [1, 2, 3] | 1 + 2 | 1 | [1, 2, 3] | 2 + 2 | 1 | [1, 2, 3] | 3 + 3 | 1 | [1, 2, 3] | 1 + 3 | 1 | [1, 2, 3] | 2 + 3 | 1 | [1, 2, 3] | 3 + 3 | 1 | [2, 3, 4, 5] | 2 + 3 | 1 | [2, 3, 4, 5] | 3 + 3 | 1 | [2, 3, 4, 5] | 4 + 3 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [1, 2, 3] | 1 + 4 | 1 | [1, 2, 3] | 2 + 4 | 1 | [1, 2, 3] | 3 + 4 | 1 | [2, 3, 4, 5] | 2 + 4 | 1 | [2, 3, 4, 5] | 3 + 4 | 1 | [2, 3, 4, 5] | 4 + 4 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [3, 4, 5, 6] | 3 + 4 | 1 | [3, 4, 5, 6] | 4 + 4 | 1 | [3, 4, 5, 6] | 5 + 4 | 1 | [3, 4, 5, 6] | 6 + 2 | 2 | [1, 2, 3] | 2 + 2 | 2 | [1, 2, 3] | 3 + 3 | 2 | [1, 2, 3] | 2 + 3 | 2 | [1, 2, 3] | 3 + 3 | 2 | [2, 3, 4, 5] | 2 + 3 | 2 | [2, 3, 4, 5] | 3 + 3 | 2 | [2, 3, 4, 5] | 4 + 3 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [1, 2, 3] | 2 + 4 | 2 | [1, 2, 3] | 3 + 4 | 2 | [2, 3, 4, 5] | 2 + 4 | 2 | [2, 3, 4, 5] | 3 + 4 | 2 | [2, 3, 4, 5] | 4 + 4 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [3, 4, 5, 6] | 3 + 4 | 2 | [3, 4, 5, 6] | 4 + 4 | 2 | [3, 4, 5, 6] | 5 + 4 | 2 | [3, 4, 5, 6] | 6 + 2 | 3 | [1, 2, 3] | 3 + 3 | 3 | [1, 2, 3] | 3 + 3 | 3 | [2, 3, 4, 5] | 3 + 3 | 3 | [2, 3, 4, 5] | 4 + 3 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [1, 2, 3] | 3 + 4 | 3 | [2, 3, 4, 5] | 3 + 4 | 3 | [2, 3, 4, 5] | 4 + 4 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [3, 4, 5, 6] | 3 + 4 | 3 | [3, 4, 5, 6] | 4 + 4 | 3 | [3, 4, 5, 6] | 5 + 4 | 3 | [3, 4, 5, 6] | 6 +(52 rows) + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; +ERROR: could not find jsonpath variable "x" +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); + json_value +------------ + foo +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_query +------------ + 123 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); + json_query +------------ + [123] +(1 row) + +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +ERROR: syntax error at or near " " of jsonpath input +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); +ERROR: only string constants supported in JSON_TABLE path specification +LINE 1: SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || '... + ^ diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql index 4f30fa46b9..df4a430d88 100644 --- a/src/test/regress/sql/json_sqljson.sql +++ b/src/test/regress/sql/json_sqljson.sql @@ -9,3 +9,7 @@ SELECT JSON_VALUE(NULL FORMAT JSON, '$'); -- JSON_QUERY SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + +-- JSON_TABLE + +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql index b8a6148b7b..fbd86f2084 100644 --- a/src/test/regress/sql/jsonb_sqljson.sql +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -325,3 +325,632 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; + +-- JSON_TABLE + +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); + +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); + +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); + +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + +-- JSON_TABLE: Test backward parsing + +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); + +\sv jsonb_table_view + +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; + +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); + +-- JSON_TABLE: nested paths and plans + +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; + +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; + +-- JSON_TABLE: plan validation + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; + +-- JSON_TABLE: plan execution + +CREATE TEMP TABLE jsonb_table_test (js jsonb); + +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); + +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; + +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 211a223193..246fbbd035 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1303,6 +1303,7 @@ JsonPathKeyword JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback +JsonPathSpec JsonPathString JsonPathVarCallback JsonPathVariable @@ -1311,6 +1312,17 @@ JsonQuotes JsonReturning JsonSemAction JsonSerializeExpr +JsonTable +JsonTableColumn +JsonTableColumnType +JsonTableContext +JsonTableJoinState +JsonTableParent +JsonTablePlan +JsonTablePlanJoinType +JsonTablePlanType +JsonTableScanState +JsonTableSibling JsonTokenType JsonTransformStringValuesAction JsonTypeCategory @@ -2766,6 +2778,7 @@ TableFunc TableFuncRoutine TableFuncScan TableFuncScanState +TableFuncType TableInfo TableLikeClause TableSampleClause -- 2.35.3
v9-0003-SQL-JSON-query-functions.patch (application/octet-stream, 209.0K) download From e97e972472956335d87a798a8b610050b73795a5 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Tue, 18 Jul 2023 17:58:35 +0900 Subject: [PATCH v9 3/5] SQL/JSON query functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This introduces the SQL/JSON functions for querying JSON data using jsonpath expressions. The functions are: JSON_EXISTS() JSON_QUERY() JSON_VALUE() All of these functions only operate on jsonb. The workaround for now is to cast the argument to jsonb. JSON_EXISTS() tests if the jsonpath expression applied to the jsonb value yields any values. JSON_VALUE() must return a single value, and an error occurs if it tries to return multiple values. JSON_QUERY() must return a json object or array, and there are various WRAPPER options for handling scalar or multi-value results. Both these functions have options for handling EMPTY and ERROR conditions. Author: Nikita Glukhov <n.gluhov@postgrespro.ru> Author: Teodor Sigaev <teodor@sigaev.ru> Author: Oleg Bartunov <obartunov@gmail.com> Author: Alexander Korotkov <aekorotkov@gmail.com> Author: Andrew Dunstan <andrew@dunslane.net> Author: Amit Langote <amitlangote09@gmail.com> Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- doc/src/sgml/func.sgml | 147 +++ src/backend/executor/execExpr.c | 432 ++++++++ src/backend/executor/execExprInterp.c | 502 ++++++++- src/backend/jit/llvm/llvmjit_expr.c | 250 +++++ src/backend/jit/llvm/llvmjit_types.c | 5 + src/backend/nodes/makefuncs.c | 15 + src/backend/nodes/nodeFuncs.c | 183 ++++ src/backend/optimizer/path/costsize.c | 3 +- src/backend/optimizer/util/clauses.c | 19 + src/backend/parser/gram.y | 348 ++++++- src/backend/parser/parse_collate.c | 7 + src/backend/parser/parse_expr.c | 519 ++++++++- src/backend/parser/parse_target.c | 15 + src/backend/utils/adt/formatting.c | 44 + src/backend/utils/adt/jsonb.c | 62 ++ src/backend/utils/adt/jsonfuncs.c | 170 ++- src/backend/utils/adt/jsonpath.c | 255 +++++ src/backend/utils/adt/jsonpath_exec.c | 391 ++++++- src/backend/utils/adt/ruleutils.c | 136 +++ src/include/executor/execExpr.h | 172 +++ src/include/executor/executor.h | 1 + src/include/nodes/execnodes.h | 3 + src/include/nodes/makefuncs.h | 1 + src/include/nodes/parsenodes.h | 48 + src/include/nodes/primnodes.h | 109 ++ src/include/parser/kwlist.h | 11 + src/include/utils/formatting.h | 2 +- src/include/utils/jsonb.h | 3 + src/include/utils/jsonfuncs.h | 5 + src/include/utils/jsonpath.h | 27 + src/interfaces/ecpg/preproc/ecpg.trailer | 28 + src/test/regress/expected/json_sqljson.out | 18 + src/test/regress/expected/jsonb_sqljson.out | 1042 +++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/json_sqljson.sql | 11 + src/test/regress/sql/jsonb_sqljson.sql | 327 ++++++ src/tools/pgindent/typedefs.list | 14 + 37 files changed, 5234 insertions(+), 93 deletions(-) create mode 100644 src/test/regress/expected/json_sqljson.out create mode 100644 src/test/regress/expected/jsonb_sqljson.out create mode 100644 src/test/regress/sql/json_sqljson.sql create mode 100644 src/test/regress/sql/jsonb_sqljson.sql diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index b8102eee22..ce001c4a03 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16989,6 +16989,153 @@ array w/o UK? | t </tbody> </tgroup> </table> + + <para> + <xref linkend="functions-sqljson-querying"/> details the SQL/JSON + functions that can be used to query JSON data. + </para> + + <note> + <para> + SQL/JSON paths can only be applied to the <type>jsonb</type> type, so it + might be necessary to cast the <replaceable>context_item</replaceable> + argument of these functions to <type>jsonb</type>. + </para> + </note> + + <table id="functions-sqljson-querying"> + <title>SQL/JSON Query Functions</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="func_table_entry"><para role="func_signature"> + Function signature + </para> + <para> + Description + </para> + <para> + Example(s) + </para></entry> + </row> + </thead> + <tbody> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_exists</primary></indexterm> + <function>json_exists</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>TRUE</literal> | <literal>FALSE</literal> |<literal> UNKNOWN</literal> | <literal>ERROR</literal> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns true if the SQL/JSON <replaceable>path_expression</replaceable> + applied to the <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s yields any items. + The <literal>ON ERROR</literal> clause specifies what is returned if + an error occurs. Note that if the <replaceable>path_expression</replaceable> + is <literal>strict</literal>, an error is generated if it yields no items. + The default value is <literal>UNKNOWN</literal> which causes a NULL + result. + </para> + <para> + <literal>json_exists(jsonb '{"key1": [1,2,3]}', 'strict $.key1[*] ? (@ > 2)')</literal> + <returnvalue>t</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'lax $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>f</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'strict $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>ERROR: jsonpath array subscript is out of bounds</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_value</primary></indexterm> + <function>json_value</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> + <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. The extracted value must be + a single <acronym>SQL/JSON</acronym> scalar item. For results that + are objects or arrays, use the <function>json_query</function> + function instead. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + The <literal>ON EMPTY</literal> clause specifies the behavior if the + <replaceable>path_expression</replaceable> yields no value at all. + The <literal>ON ERROR</literal> clause specifies the behavior if an + error occurs as a result of <type>jsonpath</type> evaluation + (including cast to the output type) or execution of + <literal>ON EMPTY</literal> behavior (that was caused by empty result + of <type>jsonpath</type> evaluation). + </para> + <para> + <literal>json_value(jsonb '"123.45"', '$' RETURNING float)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_value(jsonb '"03:04 2015-02-01"', '$.datetime("HH24:MI&nbsp;YYYY-MM-DD")' RETURNING date)</literal> + <returnvalue>2015-02-01</returnvalue> + </para> + <para> + <literal>json_value(jsonb '[1,2]', 'strict $[*]' DEFAULT 9 ON ERROR)</literal> + <returnvalue>9</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_query</primary></indexterm> + <function>json_query</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional> + <optional> { <literal>WITHOUT</literal> | <literal>WITH</literal> { <literal>CONDITIONAL</literal> | <optional><literal>UNCONDITIONAL</literal></optional> } } <optional> <literal>ARRAY</literal> </optional> <literal>WRAPPER</literal> </optional> + <optional> { <literal>KEEP</literal> | <literal>OMIT</literal> } <literal>QUOTES</literal> <optional> <literal>ON SCALAR STRING</literal> </optional> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. + This function must return a JSON string, so if the path expression + returns multiple SQL/JSON items, you must wrap the result using the + <literal>WITH WRAPPER</literal> clause. If the wrapper is + <literal>UNCONDITIONAL</literal>, an array wrapper will always + be applied, even if the returned value is already a single JSON object + or array, but if it is <literal>CONDITIONAL</literal>, it will not be + applied to a single array or object. <literal>UNCONDITIONAL</literal> + is the default. + If the result is a scalar string, by default the value returned will have + surrounding quotes making it a valid JSON value. However, this behavior + is reversed if <literal>OMIT QUOTES</literal> is specified. + The <literal>ON ERROR</literal> and <literal>ON EMPTY</literal> + clauses have similar semantics to those clauses for + <function>json_value</function>. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + </para> + <para> + <literal>json_query(jsonb '[1,[2,3],null]', 'lax $[*][1]' WITH CONDITIONAL WRAPPER)</literal> + <returnvalue>[3]</returnvalue> + </para></entry> + </row> + </tbody> + </tgroup> + </table> + </sect2> <sect2 id="functions-sqljson-path"> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 6ca4098bef..d3d2ce00d1 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -49,6 +49,7 @@ #include "utils/builtins.h" #include "utils/datum.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -88,6 +89,16 @@ static void ExecBuildAggTransCall(ExprState *state, AggState *aggstate, FunctionCallInfo fcinfo, AggStatePerTrans pertrans, int transno, int setno, int setoff, bool ishash, bool nullcheck); +static void ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch); +static Datum GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null); +static JsonCoercionState *ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull); +static JsonItemCoercionsState *ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull); /* @@ -2411,6 +2422,14 @@ ExecInitExprRec(Expr *node, ExprState *state, break; } + case T_JsonExpr: + { + JsonExpr *jexpr = castNode(JsonExpr, node); + + ExecInitJsonExpr(jexpr, state, resv, resnull, &scratch); + break; + } + case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -4178,3 +4197,416 @@ ExecBuildParamSetEqual(TupleDesc desc, return state; } + +/* + * Push steps to evaluate a JsonExpr and its various subsidiary expressions. + */ +static void +ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch) +{ + JsonExprState *jsestate = palloc0(sizeof(JsonExprState)); + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + ListCell *argexprlc; + ListCell *argnamelc; + int skip_step_off = -1; + int passing_args_step_off = -1; + int coercion_step_off = -1; + int coercion_finish_step_off = -1; + int behavior_step_off = -1; + int onempty_expr_step_off = -1; + int onempty_jump_step_off = -1; + int onerror_expr_step_off = -1; + int onerror_jump_step_off = -1; + List *adjust_jumps = NIL; + ListCell *lc; + ExprEvalStep *as; + + jsestate->jsexpr = jexpr; + + /* + * Add steps to compute formatted_expr, pathspec, and PASSING arg + * expressions as things that must be evaluated *before* the actual JSON + * path expression. + */ + ExecInitExprRec((Expr *) jexpr->formatted_expr, state, + &pre_eval->formatted_expr.value, + &pre_eval->formatted_expr.isnull); + ExecInitExprRec((Expr *) jexpr->path_spec, state, + &pre_eval->pathspec.value, + &pre_eval->pathspec.isnull); + + /* + * Before pushing steps for PASSING args, push a step to decide whether to + * skip evaluating the args and the JSON path expression depending on + * whether either of formatted_expr and pathspec is NULL; see + * ExecEvalJsonExprSkip(). + */ + scratch->opcode = EEOP_JSONEXPR_SKIP; + scratch->d.jsonexpr_skip.jsestate = jsestate; + skip_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* PASSING args. */ + jsestate->pre_eval.args = NIL; + passing_args_step_off = state->steps_len; + forboth(argexprlc, jexpr->passing_values, + argnamelc, jexpr->passing_names) + { + Expr *argexpr = (Expr *) lfirst(argexprlc); + String *argname = lfirst_node(String, argnamelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(argname->sval); + var->typid = exprType((Node *) argexpr); + var->typmod = exprTypmod((Node *) argexpr); + + ExecInitExprRec((Expr *) argexpr, state, &var->value, &var->isnull); + + pre_eval->args = lappend(pre_eval->args, var); + } + + /* + * Step for the actual JSON path evaluation; see ExecEvalJson() and + * ExecEvalJsonExpr(). + */ + scratch->opcode = EEOP_JSONEXPR_PATH; + scratch->d.jsonexpr.jsestate = jsestate; + ExprEvalPushStep(state, scratch); + + /* + * Push steps to control the evaluation of expressions based on the result + * of JSON path evaluation. + */ + + /* + * Step to handle ON ERROR and ON EMPTY behavior; see + * ExecEvalJsonExprBehavior(). + */ + scratch->opcode = EEOP_JSONEXPR_BEHAVIOR; + scratch->d.jsonexpr_behavior.jsestate = jsestate; + behavior_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Step(s) to evaluate ON EMPTY expression */ + onempty_expr_step_off = state->steps_len; + if (jexpr->on_empty && + jexpr->on_empty->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_empty->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_empty->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_empty, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onempty_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + } + } + + /* Step(s) to evaluate ON ERROR expression */ + onerror_expr_step_off = state->steps_len; + if (jexpr->on_error && + jexpr->on_error->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_error->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_error->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_error, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onerror_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set later */ + ExprEvalPushStep(state, scratch); + } + } + + /* + * Step to handle applying coercion to the JSON item returned by + * EEOP_JSONEXPR_PATH or to the ON EMPTY/ERROR expression as + * EEOP_JSONEXPR_BEHAVIOR decides; see ExecEvalJsonExprCoercion(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Initialize coercion expression(s). */ + if (jexpr->result_coercion) + { + jsestate->result_jcstate = + ExecInitJsonCoercion(scratch, state, jexpr->result_coercion, + resv, resnull); + /* See the comment above. */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* computed later */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + if (jexpr->coercions) + { + /* + * ExecPrepareJsonItemCoercion() called by ExecEvalJsonExpr() chooses + * one for a given JSON item returned by JsonPathValue(). + */ + jsestate->item_jcstates = + ExecInitJsonItemCoercions(scratch, state, jexpr->coercions, + resv, resnull); + } + + /* + * And a step to clean up after the coercion step; see + * ExecEvalJsonExprCoercionFinish(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION_FINISH; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_finish_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* + * Adjust jump target addresses in various post-eval steps now that we + * have all the steps in place. + */ + + /* EEOP_JSONEXPR_SKIP */ + Assert(skip_step_off >= 0); + as = &state->steps[skip_step_off]; + as->d.jsonexpr_skip.jump_coercion = coercion_step_off; + as->d.jsonexpr_skip.jump_passing_args = passing_args_step_off; + + /* EEOP_JSONEXPR_BEHAVIOR */ + Assert(behavior_step_off >= 0); + as = &state->steps[behavior_step_off]; + as->d.jsonexpr_behavior.jump_onerror_expr = onerror_expr_step_off; + as->d.jsonexpr_behavior.jump_onempty_expr = onempty_expr_step_off; + as->d.jsonexpr_behavior.jump_coercion = coercion_step_off; + as->d.jsonexpr_behavior.jump_skip_coercion = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION */ + Assert(coercion_step_off >= 0); + as = &state->steps[coercion_step_off]; + as->d.jsonexpr_coercion.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION_FINISH */ + Assert(coercion_finish_step_off >= 0); + as = &state->steps[coercion_finish_step_off]; + as->d.jsonexpr_coercion_finish.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion_finish.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JUMP steps */ + + /* + * Ones after ON EMPTY and ON ERROR non-default expressions should jump to + * the coercion step. + */ + if (onempty_jump_step_off >= 0) + { + as = &state->steps[onempty_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + if (onerror_jump_step_off >= 0) + { + as = &state->steps[onerror_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + + /* The rest should jump to the end. */ + foreach(lc, adjust_jumps) + { + as = &state->steps[lfirst_int(lc)]; + as->d.jump.jumpdone = state->steps_len; + } + + /* + * Set RETURNING type's input function used by ExecEvalJsonExprCoercion(). + */ + if (jexpr->omit_quotes || + (jexpr->result_coercion && jexpr->result_coercion->via_io)) + { + Oid typinput; + FmgrInfo *finfo; + + /* lookup the result type's input function */ + getTypeInputInfo(jexpr->returning->typid, &typinput, + &jsestate->input.typioparam); + finfo = palloc0(sizeof(FmgrInfo)); + fmgr_info(typinput, finfo); + jsestate->input.finfo = finfo; + } +} + +/* + * Evaluate a JSON error/empty behavior result. + */ +static Datum +GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null) +{ + *is_null = false; + + switch (behavior->btype) + { + case JSON_BEHAVIOR_EMPTY_ARRAY: + return JsonbPGetDatum(JsonbMakeEmptyArray()); + + case JSON_BEHAVIOR_EMPTY_OBJECT: + return JsonbPGetDatum(JsonbMakeEmptyObject()); + + case JSON_BEHAVIOR_TRUE: + return BoolGetDatum(true); + + case JSON_BEHAVIOR_FALSE: + return BoolGetDatum(false); + + case JSON_BEHAVIOR_NULL: + case JSON_BEHAVIOR_UNKNOWN: + case JSON_BEHAVIOR_EMPTY: + *is_null = true; + return (Datum) 0; + + case JSON_BEHAVIOR_DEFAULT: + /* Always handled in the caller. */ + Assert(false); + return (Datum) 0; + + default: + elog(ERROR, "unrecognized SQL/JSON behavior %d", behavior->btype); + return (Datum) 0; + } +} + +/* Initialize one JsonCoercion for execution. */ +static JsonCoercionState * +ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull) +{ + JsonCoercionState *jcstate = palloc0(sizeof(JsonCoercionState)); + + jcstate->coercion = coercion; + if (coercion && coercion->expr) + { + Datum *save_innermost_caseval; + bool *save_innermost_casenull; + + jcstate->jump_eval_expr = state->steps_len; + + /* Push step(s) to compute cstate->coercion. */ + save_innermost_caseval = state->innermost_caseval; + save_innermost_casenull = state->innermost_casenull; + + state->innermost_caseval = resv; + state->innermost_casenull = resnull; + + ExecInitExprRec((Expr *) coercion->expr, state, resv, resnull); + + state->innermost_caseval = save_innermost_caseval; + state->innermost_casenull = save_innermost_casenull; + } + else + jcstate->jump_eval_expr = -1; + + return jcstate; +} + +/* + * Push steps to evaluate coercions from a given JsonItemCoercions, which + * contains all possible coercions that may need to be applied to JSON + * items coming from evaluating the parent JsonExpr. + */ +static JsonItemCoercionsState * +ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull) +{ + JsonCoercion **coercion; + JsonItemCoercionsState *item_jcstates = palloc0(sizeof(JsonItemCoercionsState)); + JsonCoercionState **item_jcstate; + ExprEvalStep *as; + List *adjust_jumps = NIL; + ListCell *lc; + + /* Push the steps of individual coercions. */ + for (coercion = &coercions->null, + item_jcstate = &item_jcstates->null; + coercion <= &coercions->composite; + coercion++, item_jcstate++) + { + *item_jcstate = ExecInitJsonCoercion(scratch, state, *coercion, + resv, resnull); + + /* Emit JUMP step to skip past other coercions' steps. */ + scratch->opcode = EEOP_JUMP; + + /* + * Remember JUMP step address to set the actual jump target address + * below. + */ + adjust_jumps = lappend_int(adjust_jumps, state->steps_len); + ExprEvalPushStep(state, scratch); + } + + foreach(lc, adjust_jumps) + { + int jump_step_id = lfirst_int(lc); + + as = &state->steps[jump_step_id]; + as->d.jump.jumpdone = state->steps_len; + } + + return item_jcstates; +} diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 76e59691e5..4c97e714ea 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -74,6 +74,7 @@ #include "utils/json.h" #include "utils/jsonb.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/timestamp.h" @@ -152,6 +153,9 @@ static TupleDesc get_cached_rowtype(Oid type_id, int32 typmod, bool *changed); static void ExecEvalRowNullInt(ExprState *state, ExprEvalStep *op, ExprContext *econtext, bool checkisnull); +static Datum ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate); /* fast-path evaluation functions */ static Datum ExecJustInnerVar(ExprState *state, ExprContext *econtext, bool *isnull); @@ -480,6 +484,11 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_XMLEXPR, &&CASE_EEOP_JSON_CONSTRUCTOR, &&CASE_EEOP_IS_JSON, + &&CASE_EEOP_JSONEXPR_SKIP, + &&CASE_EEOP_JSONEXPR_PATH, + &&CASE_EEOP_JSONEXPR_BEHAVIOR, + &&CASE_EEOP_JSONEXPR_COERCION, + &&CASE_EEOP_JSONEXPR_COERCION_FINISH, &&CASE_EEOP_AGGREF, &&CASE_EEOP_GROUPING_FUNC, &&CASE_EEOP_WINDOW_FUNC, @@ -1186,8 +1195,14 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) /* second and third arguments are already set up */ fcinfo_in->isnull = false; + + /* Pass down soft-error capture node if any. */ + fcinfo_in->context = state->escontext; + d = FunctionCallInvoke(fcinfo_in); *op->resvalue = d; + if (SOFT_ERROR_OCCURRED(state->escontext)) + *op->resnull = true; /* Should get null result if and only if str is NULL */ if (str == NULL) @@ -1195,7 +1210,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) Assert(*op->resnull); Assert(fcinfo_in->isnull); } - else + else if (!SOFT_ERROR_OCCURRED(state->escontext)) { Assert(!*op->resnull); Assert(!fcinfo_in->isnull); @@ -1543,6 +1558,38 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_JSONEXPR_PATH) + { + /* too complex for an inline implementation */ + ExecEvalJsonExpr(state, op, econtext); + EEO_NEXT(); + } + + EEO_CASE(EEOP_JSONEXPR_SKIP) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprSkip(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_BEHAVIOR) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprBehavior(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercion(state, op, econtext, + *op->resvalue, *op->resnull)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION_FINISH) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercionFinish(state, op)); + } + EEO_CASE(EEOP_AGGREF) { /* @@ -3745,7 +3792,7 @@ ExecEvalConstraintCheck(ExprState *state, ExprEvalStep *op) { if (!*op->d.domaincheck.checknull && !DatumGetBool(*op->d.domaincheck.checkvalue)) - ereport(ERROR, + errsave(state->escontext, (errcode(ERRCODE_CHECK_VIOLATION), errmsg("value for domain %s violates check constraint \"%s\"", format_type_be(op->d.domaincheck.resulttype), @@ -4138,6 +4185,457 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op) *op->resvalue = BoolGetDatum(res); } +/* + * Evaluate given JsonExpr by performing the specified JSON operation. + * + * This also populates the JsonExprPostEvalState with the information needed + * by the subsequent steps that handle the specified JsonBehavior. + */ +void +ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) +{ + JsonExprState *jsestate = op->d.jsonexpr.jsestate; + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonExpr *jexpr = jsestate->jsexpr; + Datum item; + Datum res = (Datum) 0; + bool resnull = true; + JsonPath *path; + bool *error; + bool *empty; + + item = pre_eval->formatted_expr.value; + path = DatumGetJsonPathP(pre_eval->pathspec.value); + + /* Reset JsonExprPostEvalState for this evaluation. */ + memset(post_eval, 0, sizeof(*post_eval)); + + /* Respect ON ERROR behavior during path evaluation. */ + jsestate->throw_error = (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR); + + /* + * Be sure to save the error (if needed) and empty statuses for the + * EEOP_JSONEXPR_BEHAVIOR step to peruse. + */ + error = !jsestate->throw_error ? &post_eval->error : NULL; + empty = &post_eval->empty; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + res = JsonPathQuery(item, path, jexpr->wrapper, empty, error, + pre_eval->args); + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + resnull = !DatumGetPointer(res); + break; + + case JSON_VALUE_OP: + { + JsonbValue *jbv = JsonPathValue(item, path, empty, error, + pre_eval->args); + + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + if (!jbv) /* NULL or empty */ + { + resnull = true; + break; + } + + Assert(!*empty); + + resnull = false; + + /* coerce scalar item to the output type */ + if (jexpr->returning->typid == JSONOID || + jexpr->returning->typid == JSONBOID) + { + /* Use result coercion from json[b] to the output type */ + res = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + break; + } + + /* + * Error out if no cast exists to coerce SQL/JSON item to the + * the output type. + */ + Assert(post_eval->item_jcstate == NULL); + res = ExecPrepareJsonItemCoercion(jbv, + jsestate->item_jcstates, + &post_eval->item_jcstate); + if (post_eval->item_jcstate && + post_eval->item_jcstate->coercion && + (post_eval->item_jcstate->coercion->via_io || + post_eval->item_jcstate->coercion->via_populate)) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * Coercion via I/O means here that the cast to the target + * type simply does not exist. + */ + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE), + errmsg("SQL/JSON item cannot be cast to target type"))); + } + break; + } + + case JSON_EXISTS_OP: + { + bool exists = JsonPathExists(item, path, + pre_eval->args, + error); + + resnull = error && *error; + res = BoolGetDatum(exists); + break; + } + + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * If the ON EMPTY behavior is to cause an error, do so here. Other + * behaviors will be handled by the caller. + */ + if (*empty) + { + Assert(jexpr->on_empty); /* it is not JSON_EXISTS */ + + if (jexpr->on_empty->btype == JSON_BEHAVIOR_ERROR) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + ereport(ERROR, + (errcode(ERRCODE_NO_SQL_JSON_ITEM), + errmsg("no SQL/JSON item"))); + } + } + + *op->resvalue = res; + *op->resnull = resnull; +} + +/* + * Skip calling ExecEvalJson() on the given JsonExpr? + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_skip.jsestate; + + /* + * Skip if either of the input expressions has turned out to be NULL, + * though do execute domain checks for NULLs, which are handled by the + * coercion step. + */ + if (jsestate->pre_eval.formatted_expr.isnull || + jsestate->pre_eval.pathspec.isnull) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_skip.jump_coercion; + } + + /* + * Go evaluate the PASSING args if any and subsequently JSON path itself. + */ + return op->d.jsonexpr_skip.jump_passing_args; +} + +/* + * Returns the step address to perform the JsonBehavior applicable to + * the JSON item that resulted from evaluating the given JsonExpr. + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_behavior.jsestate; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonBehavior *behavior = NULL; + int jump_to = -1; + + if (post_eval->error || post_eval->coercion_error) + { + behavior = jsestate->jsexpr->on_error; + jump_to = op->d.jsonexpr_behavior.jump_onerror_expr; + } + else if (post_eval->empty) + { + behavior = jsestate->jsexpr->on_empty; + jump_to = op->d.jsonexpr_behavior.jump_onempty_expr; + } + else if (!post_eval->coercion_done) + { + /* + * If no error or the JSON item is not empty, directly go to the + * coercion step to coerce the item as is. + */ + return op->d.jsonexpr_behavior.jump_coercion; + } + + Assert(behavior); + + /* + * Set up for coercion step that will run to coerce a non-default behavior + * value. It should use result_coercion, if any. Errors that may occur + * should be thrown for JSON ops other than JSON_VALUE_OP. + */ + if (behavior->btype != JSON_BEHAVIOR_DEFAULT) + { + post_eval->item_jcstate = NULL; + jsestate->throw_error = (jsestate->jsexpr->op != JSON_VALUE_OP); + } + + Assert(jump_to >= 0); + return jump_to; +} + +/* + * Evaluate or return the step address to evaluate a coercion of a JSON item + * to the target type. The former if the coercion must be done right away by + * calling its input function or by calling json_populate_type(). + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull) +{ + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonExpr *jexpr = jsestate->jsexpr; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonCoercionState *item_jcstate = post_eval->item_jcstate; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + + if (item_jcstate == NULL && + jsestate->jsexpr->op != JSON_EXISTS_OP) + { + ErrorSaveContext escontext = {T_ErrorSaveContext}; + Node *escontext_p; + JsonCoercion *coercion; + Jsonb *jb; + + escontext_p = !jsestate->throw_error ? (Node *) &escontext : NULL; + coercion = result_jcstate ? result_jcstate->coercion : NULL; + jb = resnull ? NULL : DatumGetJsonbP(res); + + if ((coercion && coercion->via_io) || + (jexpr->omit_quotes && !resnull && + JB_ROOT_IS_SCALAR(jb))) + { + /* strip quotes and call typinput function */ + char *str = resnull ? NULL : JsonbUnquote(jb); + bool type_is_domain; + + type_is_domain = (getBaseType(jexpr->returning->typid) != + jexpr->returning->typid); + + /* + * Catch errors only if the type is not a domain, because errors + * caused by a domain's constraint failure must be thrown right + * away. + */ + if (!InputFunctionCallSafe(jsestate->input.finfo, str, + jsestate->input.typioparam, + jexpr->returning->typmod, + !type_is_domain ? escontext_p : NULL, + op->resvalue)) + { + post_eval->error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + else if (coercion && coercion->via_populate) + { + *op->resvalue = json_populate_type(res, JSONBOID, + jexpr->returning->typid, + jexpr->returning->typmod, + &post_eval->cache, + econtext->ecxt_per_query_memory, + op->resnull, + escontext_p); + if (SOFT_ERROR_OCCURRED(escontext_p)) + { + post_eval->error = true; + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + } + + if (!jsestate->throw_error) + { + post_eval->escontext.type = T_ErrorSaveContext; + state->escontext = (Node *) &post_eval->escontext; + } + + if (item_jcstate && item_jcstate->jump_eval_expr >= 0) + return item_jcstate->jump_eval_expr; + else if (result_jcstate && result_jcstate->jump_eval_expr >= 0) + return result_jcstate->jump_eval_expr; + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; +} + +/* + * Checks if the coercion evaluation led to an error. If an error did occur, + * this returns the address of the step that handles the error, otherwise + * the step after the coercion step, which finishes the JsonExpr processing. + */ +int +ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op) +{ + JsonExprPostEvalState *post_eval = + &op->d.jsonexpr_coercion_finish.jsestate->post_eval; + + if (SOFT_ERROR_OCCURRED(state->escontext)) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + post_eval->coercion_error = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_error; + } + + /* Reset. */ + state->escontext = NULL; + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_done; +} + +/* + * Prepare SQL/JSON item coercion to the output type. Returned a datum of the + * corresponding SQL type and a pointer to the coercion state. + */ +static Datum +ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate) +{ + JsonCoercionState *item_jcstate; + Datum res; + JsonbValue buf; + + if (item->type == jbvBinary && + JsonContainerIsScalar(item->val.binary.data)) + { + bool res PG_USED_FOR_ASSERTS_ONLY; + + res = JsonbExtractScalar(item->val.binary.data, &buf); + item = &buf; + Assert(res); + } + + /* get coercion state reference and datum of the corresponding SQL type */ + switch (item->type) + { + case jbvNull: + item_jcstate = item_jcstates->null; + res = (Datum) 0; + break; + + case jbvString: + item_jcstate = item_jcstates->string; + res = PointerGetDatum(cstring_to_text_with_len(item->val.string.val, + item->val.string.len)); + break; + + case jbvNumeric: + item_jcstate = item_jcstates->numeric; + res = NumericGetDatum(item->val.numeric); + break; + + case jbvBool: + item_jcstate = item_jcstates->boolean; + res = BoolGetDatum(item->val.boolean); + break; + + case jbvDatetime: + res = item->val.datetime.value; + switch (item->val.datetime.typid) + { + case DATEOID: + item_jcstate = item_jcstates->date; + break; + case TIMEOID: + item_jcstate = item_jcstates->time; + break; + case TIMETZOID: + item_jcstate = item_jcstates->timetz; + break; + case TIMESTAMPOID: + item_jcstate = item_jcstates->timestamp; + break; + case TIMESTAMPTZOID: + item_jcstate = item_jcstates->timestamptz; + break; + default: + elog(ERROR, "unexpected jsonb datetime type oid %u", + item->val.datetime.typid); + return (Datum) 0; + } + break; + + case jbvArray: + case jbvObject: + case jbvBinary: + item_jcstate = item_jcstates->composite; + res = JsonbPGetDatum(JsonbValueToJsonb(item)); + break; + + default: + elog(ERROR, "unexpected jsonb value type %d", item->type); + return (Datum) 0; + } + + *p_item_jcstate = item_jcstate; + + return res; +} + /* * ExecEvalGroupingFunc diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 00d7b8110b..da3870cba6 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -1860,6 +1860,256 @@ llvm_compile_expr(ExprState *state) LLVMBuildBr(b, opblocks[opno + 1]); break; + case EEOP_JSONEXPR_PATH: + build_EvalXFunc(b, mod, "ExecEvalJsonExpr", + v_state, op, v_econtext); + LLVMBuildBr(b, opblocks[opno + 1]); + break; + + case EEOP_JSONEXPR_SKIP: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprSkip() to decide if JSON path + * evaluation can be skipped. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprSkip"), + params, lengthof(params), ""); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_skip.jump_coercion, which signifies + * skipping of JSON path evaluation, else to the next step + * which must point to the steps to evaluate PASSING args, + * if any, or to the JSON path evaluation. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_skip.jump_coercion), + ""), + opblocks[op->d.jsonexpr_skip.jump_coercion], + opblocks[opno + 1]); + break; + } + + case EEOP_JSONEXPR_BEHAVIOR: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + LLVMBasicBlockRef b_jump_onerror_default; + + /* + * Call ExecEvalJsonExprBehavior() to decide if ON EMPTY + * or ON ERROR behavior must be invoked depending on what + * JSON path evaluation returned. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprBehavior"), + params, lengthof(params), ""); + + b_jump_onerror_default = + l_bb_before_v(opblocks[opno + 1], + "op.%d.jsonexpr_behavior_jump_onerror_default", opno); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_behavior.jump_coercion, else to the + * next block, one that checks whether to evaluate the ON + * ERROR default expression. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_coercion), + ""), + opblocks[op->d.jsonexpr_behavior.jump_coercion], + b_jump_onerror_default); + + /* + * Block that checks whether to evaluate the ON ERROR + * default expression. + * + * Jump to evaluate the ON ERROR default expression if the + * returned address is the same as + * jsonexpr_behavior.jump_onerror_default, else jump to + * evaluate the ON EMPTY default expression. + */ + LLVMPositionBuilderAtEnd(b, b_jump_onerror_default); + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_onerror_expr), + ""), + opblocks[op->d.jsonexpr_behavior.jump_onerror_expr], + opblocks[op->d.jsonexpr_behavior.jump_onempty_expr]); + break; + } + case EEOP_JSONEXPR_COERCION: + { + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonItemCoercionsState *item_jcstates = jsestate->item_jcstates; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + LLVMValueRef v_ret; + LLVMValueRef params[5]; + + /* + * Call ExecEvalJsonExprCoercion() to evaluate appropriate + * coercion. This will return the step address to jump + * to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + params[2] = v_econtext; + params[3] = v_resvaluep; + params[4] = v_resnullp; + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercion"), + params, lengthof(params), ""); + + /* + * Jump to handle a coercion error if the returned address + * is the same as jsonexpr_coercion.jump_coercion_error, + * else to the step after coercion (coercion done!). + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + + if (item_jcstates) + { + JsonCoercionState **item_jcstate; + int n_coercions = (int) + (item_jcstates->composite - item_jcstates->null) + 1; + int i; + LLVMBasicBlockRef *b_coercions; + + + /* + * Will create a block for each coercion below to + * check whether to evaluate the coercion's expression + * if there's one or to skip to the end if not. + */ + b_coercions = palloc((n_coercions + 1) * sizeof(LLVMBasicBlockRef)); + for (i = 0; i < n_coercions + 1; i++) + b_coercions[i] = + l_bb_before_v(opblocks[opno + 1], + "op.%d.json_item_coercion.%d", + opno, i); + + /* Jump to check first coercion */ + LLVMBuildBr(b, b_coercions[0]); + + /* + * Add conditional branches for individual coercion's + * expressions + */ + for (item_jcstate = &item_jcstates->null, i = 0; + item_jcstate <= &item_jcstates->composite; + item_jcstate++, i++) + { + /* Block for this coercion */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + + /* + * Jump to evaluate the coercion's expression if + * the address returned is the same as this + * coercion's jump_eval_expr (that is, if it is + * valid), else check the next coercion's. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const((*item_jcstate)->jump_eval_expr), + ""), + (*item_jcstate)->jump_eval_expr >= 0 ? + opblocks[(*item_jcstate)->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + b_coercions[i + 1]); + } + + /* + * A placeholder block that the last coercion's block + * might jump to, which unconditionally jumps to end + * of coercions. + */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + LLVMBuildBr(b, opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + + /* + * Jump to evaluate the result_coercion's expression if + * none of the above coercions matched, that is, if + * there's one. + */ + if (result_jcstate) + { + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(result_jcstate->jump_eval_expr), + ""), + result_jcstate->jump_eval_expr >= 0 ? + opblocks[result_jcstate->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + break; + } + + case EEOP_JSONEXPR_COERCION_FINISH: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprCoercionFinish() to check whether + * an coercion error occurred, in which case we must jump + * to whatever step handles the error. This returns the + * step address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercionFinish"), + params, lengthof(params), ""); + + /* + * Jump to the step that handles coercion error if the + * returned address is the same as + * jsonexpr_coercion_finish.jump_coercion_error, else to + * jsonexpr_coercion_finish.jump_coercion_done. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion_finish.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_done]); + break; + } + case EEOP_AGGREF: { LLVMValueRef v_aggno; diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c index 41ac4c6f45..cf3ced3427 100644 --- a/src/backend/jit/llvm/llvmjit_types.c +++ b/src/backend/jit/llvm/llvmjit_types.c @@ -135,6 +135,11 @@ void *referenced_functions[] = ExecEvalXmlExpr, ExecEvalJsonConstructor, ExecEvalJsonIsPredicate, + ExecEvalJsonExprSkip, + ExecEvalJsonExprBehavior, + ExecEvalJsonExprCoercion, + ExecEvalJsonExprCoercionFinish, + ExecEvalJsonExpr, MakeExpandedObjectReadOnlyInternal, slot_getmissingattrs, slot_getsomeattrs_int, diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 0e7e6e46d9..9c02634355 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -860,6 +860,21 @@ makeJsonValueExpr(Expr *raw_expr, Expr *formatted_expr, return jve; } +/* + * makeJsonBehavior - + * creates a JsonBehavior node + */ +JsonBehavior * +makeJsonBehavior(JsonBehaviorType type, Node *default_expr) +{ + JsonBehavior *behavior = makeNode(JsonBehavior); + + behavior->btype = type; + behavior->default_expr = default_expr; + + return behavior; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index c03f4f23e2..cd5fff2bbe 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -234,6 +234,12 @@ exprType(const Node *expr) case T_JsonIsPredicate: type = BOOLOID; break; + case T_JsonExpr: + type = ((const JsonExpr *) expr)->returning->typid; + break; + case T_JsonCoercion: + type = exprType(((const JsonCoercion *) expr)->expr); + break; case T_NullTest: type = BOOLOID; break; @@ -493,6 +499,10 @@ exprTypmod(const Node *expr) return exprTypmod((Node *) ((const JsonValueExpr *) expr)->formatted_expr); case T_JsonConstructorExpr: return ((const JsonConstructorExpr *) expr)->returning->typmod; + case T_JsonExpr: + return ((JsonExpr *) expr)->returning->typmod; + case T_JsonCoercion: + return exprTypmod(((const JsonCoercion *) expr)->expr); case T_CoerceToDomain: return ((const CoerceToDomain *) expr)->resulttypmod; case T_CoerceToDomainValue: @@ -969,6 +979,22 @@ exprCollation(const Node *expr) /* IS JSON's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + coll = InvalidOid; + else if (coercion->expr) + coll = exprCollation(coercion->expr); + else if (coercion->via_io || coercion->via_populate) + coll = coercion->collation; + else + coll = InvalidOid; + } + break; + case T_NullTest: /* NullTest's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ @@ -1205,6 +1231,21 @@ exprSetCollation(Node *expr, Oid collation) case T_JsonIsPredicate: Assert(!OidIsValid(collation)); /* result is always boolean */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + Assert(!OidIsValid(collation)); + else if (coercion->expr) + exprSetCollation(coercion->expr, collation); + else if (coercion->via_io || coercion->via_populate) + coercion->collation = collation; + else + Assert(!OidIsValid(collation)); + } + break; case T_NullTest: /* NullTest's result is boolean ... */ Assert(!OidIsValid(collation)); /* ... so never set a collation */ @@ -1508,6 +1549,15 @@ exprLocation(const Node *expr) case T_JsonIsPredicate: loc = ((const JsonIsPredicate *) expr)->location; break; + case T_JsonExpr: + { + const JsonExpr *jsexpr = (const JsonExpr *) expr; + + /* consider both function name and leftmost arg */ + loc = leftmostLoc(jsexpr->location, + exprLocation(jsexpr->formatted_expr)); + } + break; case T_NullTest: { const NullTest *nexpr = (const NullTest *) expr; @@ -2260,6 +2310,54 @@ expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + if (WALK(jexpr->formatted_expr)) + return true; + if (WALK(jexpr->result_coercion)) + return true; + if (WALK(jexpr->passing_values)) + return true; + /* we assume walker doesn't care about passing_names */ + if (jexpr->on_empty && + WALK(jexpr->on_empty->default_expr)) + return true; + if (WALK(jexpr->on_error->default_expr)) + return true; + if (WALK(jexpr->coercions)) + return true; + } + break; + case T_JsonCoercion: + return WALK(((JsonCoercion *) node)->expr); + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + + if (WALK(coercions->null)) + return true; + if (WALK(coercions->string)) + return true; + if (WALK(coercions->numeric)) + return true; + if (WALK(coercions->boolean)) + return true; + if (WALK(coercions->date)) + return true; + if (WALK(coercions->time)) + return true; + if (WALK(coercions->timetz)) + return true; + if (WALK(coercions->timestamp)) + return true; + if (WALK(coercions->timestamptz)) + return true; + if (WALK(coercions->composite)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: @@ -3259,6 +3357,54 @@ expression_tree_mutator_impl(Node *node, return (Node *) newnode; } + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + JsonExpr *newnode; + + FLATCOPY(newnode, jexpr, JsonExpr); + MUTATE(newnode->path_spec, jexpr->path_spec, Node *); + MUTATE(newnode->formatted_expr, jexpr->formatted_expr, Node *); + MUTATE(newnode->result_coercion, jexpr->result_coercion, JsonCoercion *); + MUTATE(newnode->passing_values, jexpr->passing_values, List *); + /* assume mutator does not care about passing_names */ + if (newnode->on_empty) + MUTATE(newnode->on_empty->default_expr, + jexpr->on_empty->default_expr, Node *); + MUTATE(newnode->on_error->default_expr, + jexpr->on_error->default_expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonCoercion: + { + JsonCoercion *coercion = (JsonCoercion *) node; + JsonCoercion *newnode; + + FLATCOPY(newnode, coercion, JsonCoercion); + MUTATE(newnode->expr, coercion->expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + JsonItemCoercions *newnode; + + FLATCOPY(newnode, coercions, JsonItemCoercions); + MUTATE(newnode->null, coercions->null, JsonCoercion *); + MUTATE(newnode->string, coercions->string, JsonCoercion *); + MUTATE(newnode->numeric, coercions->numeric, JsonCoercion *); + MUTATE(newnode->boolean, coercions->boolean, JsonCoercion *); + MUTATE(newnode->date, coercions->date, JsonCoercion *); + MUTATE(newnode->time, coercions->time, JsonCoercion *); + MUTATE(newnode->timetz, coercions->timetz, JsonCoercion *); + MUTATE(newnode->timestamp, coercions->timestamp, JsonCoercion *); + MUTATE(newnode->timestamptz, coercions->timestamptz, JsonCoercion *); + MUTATE(newnode->composite, coercions->composite, JsonCoercion *); + return (Node *) newnode; + } + break; case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -3945,6 +4091,43 @@ raw_expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonArgument: + return WALK(((JsonArgument *) node)->val); + case T_JsonCommon: + { + JsonCommon *jc = (JsonCommon *) node; + + if (WALK(jc->expr)) + return true; + if (WALK(jc->pathspec)) + return true; + if (WALK(jc->passing)) + return true; + } + break; + case T_JsonBehavior: + { + JsonBehavior *jb = (JsonBehavior *) node; + + if (jb->btype == JSON_BEHAVIOR_DEFAULT && + WALK(jb->default_expr)) + return true; + } + break; + case T_JsonFuncExpr: + { + JsonFuncExpr *jfe = (JsonFuncExpr *) node; + + if (WALK(jfe->common)) + return true; + if (jfe->output && WALK(jfe->output)) + return true; + if (WALK(jfe->on_empty)) + return true; + if (WALK(jfe->on_error)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index ef475d95a1..f58c275b4b 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -4609,7 +4609,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context) IsA(node, SQLValueFunction) || IsA(node, XmlExpr) || IsA(node, CoerceToDomain) || - IsA(node, NextValueExpr)) + IsA(node, NextValueExpr) || + IsA(node, JsonExpr)) { /* Treat all these as having cost 1 */ context->total.per_tuple += cpu_operator_cost; diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index da258968b8..e40cfab4b7 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -53,6 +53,7 @@ #include "utils/fmgroids.h" #include "utils/json.h" #include "utils/jsonb.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/syscache.h" @@ -412,6 +413,24 @@ contain_mutable_functions_walker(Node *node, void *context) /* Check all subnodes */ } + if (IsA(node, JsonExpr)) + { + JsonExpr *jexpr = castNode(JsonExpr, node); + Const *cnst; + + if (!IsA(jexpr->path_spec, Const)) + return true; + + cnst = castNode(Const, jexpr->path_spec); + + Assert(cnst->consttype == JSONPATHOID); + if (cnst->constisnull) + return false; + + return jspIsMutable(DatumGetJsonPathP(cnst->constvalue), + jexpr->passing_names, jexpr->passing_values); + } + if (IsA(node, SQLValueFunction)) { /* all variants of SQLValueFunction are stable */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 99e05d8548..1736ba35bd 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -278,6 +278,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MergeWhenClause *mergewhen; struct KeyActions *keyactions; struct KeyAction *keyaction; + JsonBehavior *jsbehavior; + JsonQuotes js_quotes; } %type <node> stmt toplevel_stmt schema_stmt routine_body_stmt @@ -650,14 +652,22 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_returning_clause_opt json_name_and_value json_aggregate_func + json_api_common_syntax + json_argument %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt + json_arguments %type <ival> json_encoding_clause_opt json_predicate_type_constraint + json_wrapper_behavior %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt +%type <jsbehavior> json_value_behavior + json_query_behavior + json_exists_behavior +%type <js_quotes> json_quotes_clause_opt /* * Non-keyword token types. These are hard-wired into the "flex" lexer. @@ -694,7 +704,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT - COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT + COMMITTED COMPRESSION CONCURRENTLY CONDITIONAL CONFIGURATION CONFLICT CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY COST CREATE CROSS CSV CUBE CURRENT_P CURRENT_CATALOG CURRENT_DATE CURRENT_ROLE CURRENT_SCHEMA @@ -705,8 +715,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P DOUBLE_P DROP - EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT - EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION + EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE + EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION EXTENSION EXTERNAL EXTRACT FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR @@ -721,10 +731,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION - JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG - JSON_SCALAR JSON_SERIALIZE + JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE - KEY KEYS + KEY KEYS KEEP LABEL LANGUAGE LARGE_P LAST_P LATERAL_P LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL @@ -738,7 +748,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC - OBJECT_P OF OFF OFFSET OIDS OLD ON ONLY OPERATOR OPTION OPTIONS OR + OBJECT_P OF OFF OFFSET OIDS OLD OMIT ON ONLY OPERATOR OPTION OPTIONS OR ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER @@ -747,7 +757,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION - QUOTE + QUOTE QUOTES RANGE READ REAL REASSIGN RECHECK RECURSIVE REF_P REFERENCES REFERENCING REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA @@ -758,7 +768,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); SEQUENCE SEQUENCES SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P - START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P + START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRING_P STRIP_P SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN @@ -766,7 +776,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); TREAT TRIGGER TRIM TRUE_P TRUNCATE TRUSTED TYPE_P TYPES_P - UESCAPE UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN + UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING @@ -15671,6 +15681,192 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_error = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->on_error = $10; + n->location = @1; + $$ = (Node *) n; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->location = @1; + $$ = (Node *) p; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + json_exists_behavior ON ERROR_P + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->on_error = $5; + p->location = @1; + $$ = (Node *) p; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->on_error = $8; + n->location = @1; + $$ = (Node *) n; + } ; @@ -16397,6 +16593,72 @@ opt_asymmetric: ASYMMETRIC ; /* SQL/JSON support */ +json_api_common_syntax: + json_value_expr ',' a_expr /* i.e. a json_path */ + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + + | json_value_expr ',' a_expr /* i.e. a json_path */ + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = $5; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = $7; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_arguments: + json_argument { $$ = list_make1($1); } + | json_arguments ',' json_argument { $$ = lappend($1, $3); } + ; + +json_argument: + json_value_expr AS ColLabel + { + JsonArgument *n = makeNode(JsonArgument); + + n->val = (JsonValueExpr *) $1; + n->name = $3; + $$ = (Node *) n; + } + ; + json_value_expr: a_expr json_format_clause_opt { @@ -16422,6 +16684,50 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; +json_query_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + | EMPTY_P ARRAY { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + | EMPTY_P OBJECT_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + /* non-standard, for Oracle compatibility only */ + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + ; + +json_exists_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | TRUE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_TRUE, NULL); } + | FALSE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_FALSE, NULL); } + | UNKNOWN { $$ = makeJsonBehavior(JSON_BEHAVIOR_UNKNOWN, NULL); } + ; + +json_value_behavior: + NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + ; + +/* ARRAY is a noise word */ +json_wrapper_behavior: + WITHOUT WRAPPER { $$ = JSW_NONE; } + | WITHOUT ARRAY WRAPPER { $$ = JSW_NONE; } + | WITH WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL ARRAY WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL WRAPPER { $$ = JSW_UNCONDITIONAL; } + | /* empty */ { $$ = JSW_NONE; } + ; + +json_quotes_clause_opt: + KEEP QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_KEEP; } + | KEEP QUOTES { $$ = JS_QUOTES_KEEP; } + | OMIT QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_OMIT; } + | OMIT QUOTES { $$ = JS_QUOTES_OMIT; } + | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } + ; + json_returning_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17024,6 +17330,7 @@ unreserved_keyword: | COMMIT | COMMITTED | COMPRESSION + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17060,10 +17367,12 @@ unreserved_keyword: | DOUBLE_P | DROP | EACH + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17113,6 +17422,7 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION + | KEEP | KEY | KEYS | LABEL @@ -17159,6 +17469,7 @@ unreserved_keyword: | OFF | OIDS | OLD + | OMIT | OPERATOR | OPTION | OPTIONS @@ -17189,6 +17500,7 @@ unreserved_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REASSIGN @@ -17248,6 +17560,7 @@ unreserved_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUPPORT @@ -17270,6 +17583,7 @@ unreserved_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNKNOWN | UNLISTEN @@ -17330,10 +17644,13 @@ col_name_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE | LEAST | NATIONAL | NCHAR @@ -17566,6 +17883,7 @@ bare_label_keyword: | COMMITTED | COMPRESSION | CONCURRENTLY + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17618,11 +17936,13 @@ bare_label_keyword: | DROP | EACH | ELSE + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | END_P | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17692,10 +18012,14 @@ bare_label_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE + | KEEP | KEY | KEYS | LABEL @@ -17756,6 +18080,7 @@ bare_label_keyword: | OFF | OIDS | OLD + | OMIT | ONLY | OPERATOR | OPTION @@ -17793,6 +18118,7 @@ bare_label_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REAL @@ -17861,6 +18187,7 @@ bare_label_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUBSTRING @@ -17895,6 +18222,7 @@ bare_label_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNIQUE | UNKNOWN diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c index 9f6afc351c..692e5d1225 100644 --- a/src/backend/parser/parse_collate.c +++ b/src/backend/parser/parse_collate.c @@ -691,6 +691,13 @@ assign_collations_walker(Node *node, assign_collations_context *context) &loccontext); } break; + case T_JsonExpr: + + /* + * Context item and PASSING arguments are already + * marked with collations in parse_expr.c. + */ + break; default: /* diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index e7f988dbd2..ca60b5604a 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -90,6 +90,7 @@ static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); static Node *transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr); +static Node *transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *p); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -353,6 +354,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); break; + case T_JsonFuncExpr: + result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3229,7 +3234,7 @@ makeJsonByteaToTextConversion(Node *expr, JsonFormat *format, int location) static Node * transformJsonValueExpr(ParseState *pstate, const char *constructName, JsonValueExpr *ve, JsonFormatType default_format, - Oid targettype) + Oid targettype, bool isarg) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3266,6 +3271,35 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, else format = ve->format->format_type; } + else if (isarg) + { + /* Pass SQL/JSON item types directly without conversion to json[b]. */ + switch (exprtype) + { + case TEXTOID: + case NUMERICOID: + case BOOLOID: + case INT2OID: + case INT4OID: + case INT8OID: + case FLOAT4OID: + case FLOAT8OID: + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + return expr; + + default: + if (typcategory == TYPCATEGORY_STRING) + return expr; + /* else convert argument to json[b] type */ + break; + } + + format = default_format; + } else if (exprtype == JSONOID || exprtype == JSONBOID) format = JS_FORMAT_DEFAULT; /* do not format json[b] types */ else @@ -3277,7 +3311,8 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, Node *coerced; bool cast_is_needed = OidIsValid(targettype); - if (!cast_is_needed && + if (!isarg && + !cast_is_needed && exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), @@ -3622,7 +3657,7 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) Node *val = transformJsonValueExpr(pstate, "JSON_OBJECT()", kv->value, JS_FORMAT_DEFAULT, - InvalidOid); + InvalidOid, false); args = lappend(args, key); args = lappend(args, val); @@ -3807,7 +3842,7 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) key = transformExprRecurse(pstate, (Node *) agg->arg->key); val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", agg->arg->value, - JS_FORMAT_DEFAULT, InvalidOid); + JS_FORMAT_DEFAULT, InvalidOid, false); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3863,9 +3898,8 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) Oid aggfnoid; Oid aggtype; - arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", - agg->arg, - JS_FORMAT_DEFAULT, InvalidOid); + arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", agg->arg, + JS_FORMAT_DEFAULT, InvalidOid, false); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3912,9 +3946,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) { JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, "JSON_ARRAY()", - jsval, - JS_FORMAT_DEFAULT, - InvalidOid); + jsval, JS_FORMAT_DEFAULT, + InvalidOid, false); args = lappend(args, val); } @@ -4070,7 +4103,7 @@ transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) * function-like CASTs. */ arg = transformJsonValueExpr(pstate, "JSON()", jsexpr->expr, - JS_FORMAT_JSON, returning->typid); + JS_FORMAT_JSON, returning->typid, false); } return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, @@ -4104,9 +4137,8 @@ static Node * transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) { Node *arg = transformJsonValueExpr(pstate, "JSON_SERIALIZE()", - expr->expr, - JS_FORMAT_JSON, - InvalidOid); + expr->expr, JS_FORMAT_JSON, + InvalidOid, false); JsonReturning *returning; if (expr->output) @@ -4141,3 +4173,462 @@ transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr * expr) return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), NULL, returning, false, false, expr->location); } + +/* + * Transform a JSON PASSING clause. + */ +static void +transformJsonPassingArgs(ParseState *pstate, char *constructName, + JsonFormatType format, List *args, + List **passing_values, List **passing_names) +{ + ListCell *lc; + + *passing_values = NIL; + *passing_names = NIL; + + foreach(lc, args) + { + JsonArgument *arg = castNode(JsonArgument, lfirst(lc)); + Node *expr = transformJsonValueExpr(pstate, constructName, + arg->val, format, + InvalidOid, true); + + assign_expr_collations(pstate, expr); + + *passing_values = lappend(*passing_values, expr); + *passing_names = lappend(*passing_names, makeString(arg->name)); + } +} + +/* + * Transform a JSON BEHAVIOR clause. + */ +static JsonBehavior * +transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior, + JsonBehaviorType default_behavior) +{ + JsonBehaviorType behavior_type = default_behavior; + Node *default_expr = NULL; + + if (behavior) + { + behavior_type = behavior->btype; + if (behavior_type == JSON_BEHAVIOR_DEFAULT) + default_expr = transformExprRecurse(pstate, behavior->default_expr); + } + return makeJsonBehavior(behavior_type, default_expr); +} + +/* + * Common code for JSON_VALUE, JSON_QUERY, JSON_EXISTS transformation + * into a JsonExpr node. + */ +static JsonExpr * +transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = makeNode(JsonExpr); + Node *pathspec; + JsonFormatType format; + char *constructName; + + if (func->common->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("JSON_TABLE path name is not allowed here"), + parser_errposition(pstate, func->location))); + + jsexpr->location = func->location; + jsexpr->op = func->op; + switch (jsexpr->op) + { + case JSON_VALUE_OP: + constructName = "JSON_VALUE()"; + break; + case JSON_QUERY_OP: + constructName = "JSON_QUERY()"; + break; + case JSON_EXISTS_OP: + constructName = "JSON_EXISTS()"; + break; + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); + break; + } + jsexpr->formatted_expr = transformJsonValueExpr(pstate, constructName, + func->common->expr, + JS_FORMAT_DEFAULT, + InvalidOid, false); + + assign_expr_collations(pstate, jsexpr->formatted_expr); + + /* format is determined by context item type */ + format = exprType(jsexpr->formatted_expr) == JSONBOID ? JS_FORMAT_JSONB : JS_FORMAT_JSON; + + jsexpr->result_coercion = NULL; + jsexpr->omit_quotes = false; + + jsexpr->format = func->common->expr->format; + + pathspec = transformExprRecurse(pstate, func->common->pathspec); + + jsexpr->path_spec = + coerce_to_target_type(pstate, pathspec, exprType(pathspec), + JSONPATHOID, -1, + COERCION_EXPLICIT, COERCE_IMPLICIT_CAST, + exprLocation(pathspec)); + if (!jsexpr->path_spec) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("JSON path expression must be type %s, not type %s", + "jsonpath", format_type_be(exprType(pathspec))), + parser_errposition(pstate, exprLocation(pathspec)))); + + /* transform and coerce to json[b] passing arguments */ + transformJsonPassingArgs(pstate, constructName, format, + func->common->passing, + &jsexpr->passing_values, + &jsexpr->passing_names); + + if (func->op != JSON_EXISTS_OP) + jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, + JSON_BEHAVIOR_NULL); + + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + func->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : + JSON_BEHAVIOR_NULL); + + return jsexpr; +} + +/* + * Assign default JSON returning type from the specified format or from + * the context item type. + */ +static void +assignDefaultJsonReturningType(Node *context_item, JsonFormat *context_format, + JsonReturning *ret) +{ + bool is_jsonb; + + ret->format = copyObject(context_format); + + if (ret->format->format_type == JS_FORMAT_DEFAULT) + is_jsonb = exprType(context_item) == JSONBOID; + else + is_jsonb = ret->format->format_type == JS_FORMAT_JSONB; + + ret->typid = is_jsonb ? JSONBOID : JSONOID; + ret->typmod = -1; +} + +/* + * Try to coerce expression to the output type or + * use json_populate_type() for composite, array and domain types or + * use coercion via I/O. + */ +static JsonCoercion * +coerceJsonExpr(ParseState *pstate, Node *expr, const JsonReturning *returning) +{ + char typtype; + JsonCoercion *coercion = makeNode(JsonCoercion); + + coercion->expr = coerceJsonFuncExpr(pstate, expr, returning, false); + + if (coercion->expr) + { + if (coercion->expr == expr) + coercion->expr = NULL; + + return coercion; + } + + typtype = get_typtype(returning->typid); + + if (returning->typid == RECORDOID || + typtype == TYPTYPE_COMPOSITE || + typtype == TYPTYPE_DOMAIN || + type_is_array(returning->typid)) + coercion->via_populate = true; + else + coercion->via_io = true; + + return coercion; +} + +/* + * Transform a JSON output clause of JSON_VALUE and JSON_QUERY. + */ +static void +transformJsonFuncExprOutput(ParseState *pstate, JsonFuncExpr *func, + JsonExpr *jsexpr) +{ + Node *expr = jsexpr->formatted_expr; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + /* JSON_VALUE returns text by default */ + if (func->op == JSON_VALUE_OP && !OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = TEXTOID; + jsexpr->returning->typmod = -1; + } + + if (OidIsValid(jsexpr->returning->typid)) + { + JsonReturning ret; + + if (func->op == JSON_VALUE_OP && + jsexpr->returning->typid != JSONOID && + jsexpr->returning->typid != JSONBOID) + { + /* Forced coercion via I/O for JSON_VALUE for non-JSON types */ + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = NULL; + jsexpr->result_coercion->via_io = true; + return; + } + + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, &ret); + + if (ret.typid != jsexpr->returning->typid || + ret.typmod != jsexpr->returning->typmod) + { + CaseTestExpr *cte = makeNode(CaseTestExpr); + + cte->typeId = exprType(expr); + cte->typeMod = exprTypmod(expr); + cte->collation = exprCollation(expr); + + Assert(cte->typeId == ret.typid); + Assert(cte->typeMod == ret.typmod); + + jsexpr->result_coercion = coerceJsonExpr(pstate, (Node *) cte, + jsexpr->returning); + } + } + else + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, + jsexpr->returning); +} + +/* + * Coerce an expression in JSON DEFAULT behavior to the target output type. + */ +static Node * +coerceDefaultJsonExpr(ParseState *pstate, JsonExpr *jsexpr, Node *defexpr) +{ + int location; + Oid exprtype; + + if (!defexpr) + return NULL; + + exprtype = exprType(defexpr); + location = exprLocation(defexpr); + + if (location < 0) + location = jsexpr->location; + + defexpr = coerce_to_target_type(pstate, + defexpr, + exprtype, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!defexpr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast DEFAULT expression type %s to %s", + format_type_be(exprtype), + format_type_be(jsexpr->returning->typid)), + parser_errposition(pstate, location))); + + return defexpr; +} + +/* + * Initialize SQL/JSON item coercion from the SQL type "typid" to the target + * "returning" type. + */ +static JsonCoercion * +initJsonItemCoercion(ParseState *pstate, Oid typid, + const JsonReturning *returning) +{ + Node *expr; + + if (typid == UNKNOWNOID) + { + expr = (Node *) makeNullConst(UNKNOWNOID, -1, InvalidOid); + } + else + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + + placeholder->typeId = typid; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + expr = (Node *) placeholder; + } + + return coerceJsonExpr(pstate, expr, returning); +} + +static void +initJsonItemCoercions(ParseState *pstate, JsonItemCoercions *coercions, + const JsonReturning *returning, Oid contextItemTypeId) +{ + struct + { + JsonCoercion **coercion; + Oid typid; + } *p, + coercionTypids[] = + { + {&coercions->null, UNKNOWNOID}, + {&coercions->string, TEXTOID}, + {&coercions->numeric, NUMERICOID}, + {&coercions->boolean, BOOLOID}, + {&coercions->date, DATEOID}, + {&coercions->time, TIMEOID}, + {&coercions->timetz, TIMETZOID}, + {&coercions->timestamp, TIMESTAMPOID}, + {&coercions->timestamptz, TIMESTAMPTZOID}, + {&coercions->composite, contextItemTypeId}, + {NULL, InvalidOid} + }; + + for (p = coercionTypids; p->coercion; p++) + *p->coercion = initJsonItemCoercion(pstate, p->typid, returning); +} + +/* + * Transform JSON_VALUE, JSON_QUERY, JSON_EXISTS functions into a JsonExpr node. + */ +static Node * +transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = transformJsonExprCommon(pstate, func); + const char *func_name = NULL; + Node *contextItemExpr = jsexpr->formatted_expr; + + /* + * Disallow FORMAT specification in the RETURNING clause of JSON_EXISTS() + * and JSON_VALUE(). + */ + if (func->output && + (func->op == JSON_VALUE_OP || func->op == JSON_EXISTS_OP)) + { + JsonFormat *format = func->output->returning->format; + + if(format->format_type != JS_FORMAT_DEFAULT || + format->encoding != JS_ENC_DEFAULT) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot specify FORMAT in RETURNING clause of %s", + func->op == JSON_VALUE_OP ? "JSON_VALUE()" : + "JSON_EXISTS()"), + parser_errposition(pstate, format->location))); + } + + switch (func->op) + { + case JSON_VALUE_OP: + func_name = "JSON_VALUE"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->coercions = makeNode(JsonItemCoercions); + initJsonItemCoercions(pstate, jsexpr->coercions, jsexpr->returning, + exprType(contextItemExpr)); + + break; + + case JSON_QUERY_OP: + func_name = "JSON_QUERY"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->wrapper = func->wrapper; + jsexpr->omit_quotes = func->omit_quotes; + + break; + + case JSON_EXISTS_OP: + func_name = "JSON_EXISTS"; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + if (!OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = BOOLOID; + jsexpr->returning->typmod = -1; + } + else if (jsexpr->returning->typid != BOOLOID) + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + int location = exprLocation((Node *) jsexpr); + + placeholder->typeId = BOOLOID; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = + coerce_to_target_type(pstate, (Node *) placeholder, BOOLOID, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!jsexpr->result_coercion->expr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(BOOLOID), + format_type_be(jsexpr->returning->typid)), + parser_coercion_errposition(pstate, location, (Node *) jsexpr))); + + if (jsexpr->result_coercion->expr == (Node *) placeholder) + jsexpr->result_coercion->expr = NULL; + } + break; + } + + if (exprType(contextItemExpr) != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("%s() is not yet implemented for the json type", func_name), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + return (Node *) jsexpr; +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 520d4f2a23..4f94fc69d6 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1979,6 +1979,21 @@ FigureColnameInternal(Node *node, char **name) /* make JSON_ARRAYAGG act like a regular function */ *name = "json_arrayagg"; return 2; + case T_JsonFuncExpr: + /* make SQL/JSON functions act like a regular function */ + switch (((JsonFuncExpr *) node)->op) + { + case JSON_QUERY_OP: + *name = "json_query"; + return 2; + case JSON_VALUE_OP: + *name = "json_value"; + return 2; + case JSON_EXISTS_OP: + *name = "json_exists"; + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index e27ea8ef97..188b5594e0 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -4426,6 +4426,50 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, } } +/* + * Parses the datetime format string in 'fmt_str' and returns true if it + * contains a timezone specifier, false if not. + */ +bool +datetime_format_has_tz(const char *fmt_str) +{ + bool incache; + int fmt_len = strlen(fmt_str); + int result; + FormatNode *format; + + if (fmt_len > DCH_CACHE_SIZE) + { + /* + * Allocate new memory if format picture is bigger than static cache + * and do not use cache (call parser always) + */ + incache = false; + + format = (FormatNode *) palloc((fmt_len + 1) * sizeof(FormatNode)); + + parse_format(format, fmt_str, DCH_keywords, + DCH_suff, DCH_index, DCH_FLAG, NULL); + } + else + { + /* + * Use cache buffers + */ + DCHCacheEntry *ent = DCH_cache_fetch(fmt_str, false); + + incache = true; + format = ent->format; + } + + result = DCH_datetime_type(format); + + if (!incache) + pfree(format); + + return result & DCH_ZONED; +} + /* * do_to_timestamp: shared code for to_timestamp and to_date * diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index 06ba409e64..d2b4da8ec8 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -2156,3 +2156,65 @@ jsonb_float8(PG_FUNCTION_ARGS) PG_RETURN_DATUM(retValue); } + +/* + * Construct an empty array jsonb. + */ +Jsonb * +JsonbMakeEmptyArray(void) +{ + JsonbValue jbv; + + jbv.type = jbvArray; + jbv.val.array.elems = NULL; + jbv.val.array.nElems = 0; + jbv.val.array.rawScalar = false; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Construct an empty object jsonb. + */ +Jsonb * +JsonbMakeEmptyObject(void) +{ + JsonbValue jbv; + + jbv.type = jbvObject; + jbv.val.object.pairs = NULL; + jbv.val.object.nPairs = 0; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Convert jsonb to a C-string stripping quotes from scalar strings. + */ +char * +JsonbUnquote(Jsonb *jb) +{ + if (JB_ROOT_IS_SCALAR(jb)) + { + JsonbValue v; + + (void) JsonbExtractScalar(&jb->root, &v); + + if (v.type == jbvString) + return pnstrdup(v.val.string.val, v.val.string.len); + else if (v.type == jbvBool) + return pstrdup(v.val.boolean ? "true" : "false"); + else if (v.type == jbvNumeric) + return DatumGetCString(DirectFunctionCall1(numeric_out, + PointerGetDatum(v.val.numeric))); + else if (v.type == jbvNull) + return pstrdup("null"); + else + { + elog(ERROR, "unrecognized jsonb value type %d", v.type); + return NULL; + } + } + else + return JsonbToCString(NULL, &jb->root, VARSIZE(jb)); +} diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index a4bfa5e404..2d5fa285fc 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -265,6 +265,7 @@ typedef struct PopulateArrayContext int *dims; /* dimensions */ int *sizes; /* current dimension counters */ int ndims; /* number of dimensions */ + Node *escontext; /* For soft-error capture */ } PopulateArrayContext; /* state for populate_array_json() */ @@ -442,12 +443,13 @@ static void JsValueToJsObject(JsValue *jsv, JsObject *jso); static Datum populate_composite(CompositeIOData *io, Oid typid, const char *colname, MemoryContext mcxt, HeapTupleHeader defaultval, JsValue *jsv, bool isnull); -static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv); +static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext); static void prepare_column_cache(ColumnIOData *column, Oid typid, int32 typmod, MemoryContext mcxt, bool need_scalar); static Datum populate_record_field(ColumnIOData *col, Oid typid, int32 typmod, const char *colname, MemoryContext mcxt, Datum defaultval, - JsValue *jsv, bool *isnull); + JsValue *jsv, bool *isnull, Node *escontext); static RecordIOData *allocate_record_info(MemoryContext mcxt, int ncolumns); static bool JsObjectGetField(JsObject *obj, char *field, JsValue *jsv); static void populate_recordset_record(PopulateRecordsetState *state, JsObject *obj); @@ -459,7 +461,8 @@ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims); static void populate_array_check_dimension(PopulateArrayContext *ctx, int ndim); static void populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv); static Datum populate_array(ArrayIOData *aio, const char *colname, - MemoryContext mcxt, JsValue *jsv); + MemoryContext mcxt, JsValue *jsv, Node *escontext, + bool *isnull); static Datum populate_domain(DomainIOData *io, Oid typid, const char *colname, MemoryContext mcxt, JsValue *jsv, bool isnull); @@ -2484,12 +2487,12 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) if (ndim <= 0) { if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the value of key \"%s\".", ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"))); } @@ -2506,13 +2509,13 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) appendStringInfo(&indices, "[%d]", ctx->sizes[i]); if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s of key \"%s\".", indices.data, ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s.", @@ -2520,7 +2523,11 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) } } -/* set the number of dimensions of the populated array when it becomes known */ +/* + * Set the number of dimensions of the populated array when it becomes known. + * + * Returns without doing anything if the input (ndims) is erratic. + */ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) { @@ -2531,6 +2538,10 @@ populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) if (ndims <= 0) populate_array_report_expected_array(ctx, ndims); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + ctx->ndims = ndims; ctx->dims = palloc(sizeof(int) * ndims); ctx->sizes = palloc0(sizeof(int) * ndims); @@ -2548,12 +2559,16 @@ populate_array_check_dimension(PopulateArrayContext *ctx, int ndim) if (ctx->dims[ndim] == -1) ctx->dims[ndim] = dim; /* assign dimension if not yet known */ else if (ctx->dims[ndim] != dim) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("malformed JSON array"), errdetail("Multidimensional arrays must have " "sub-arrays with matching dimensions."))); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + /* reset the current array dimension size counter */ ctx->sizes[ndim] = 0; @@ -2573,7 +2588,10 @@ populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv) ctx->aio->element_type, ctx->aio->element_typmod, NULL, ctx->mcxt, PointerGetDatum(NULL), - jsv, &element_isnull); + jsv, &element_isnull, ctx->escontext); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; accumArrayResult(ctx->astate, element, element_isnull, ctx->aio->element_type, ctx->acxt); @@ -2594,6 +2612,10 @@ populate_array_object_start(void *_state) else if (ndim < state->ctx->ndims) populate_array_report_expected_array(state->ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2611,6 +2633,10 @@ populate_array_array_end(void *_state) if (ndim < ctx->ndims) populate_array_check_dimension(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2686,6 +2712,10 @@ populate_array_scalar(void *_state, char *token, JsonTokenType tokentype) else if (ndim < ctx->ndims) populate_array_report_expected_array(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + if (ndim == ctx->ndims) { /* remember the scalar element token */ @@ -2715,7 +2745,13 @@ populate_array_json(PopulateArrayContext *ctx, char *json, int len) sem.array_element_end = populate_array_element_end; sem.scalar = populate_array_scalar; - pg_parse_json_or_ereport(state.lex, &sem); + if (!pg_parse_json_or_errsave(state.lex, &sem, ctx->escontext)) + { + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + pfree(state.lex); + return; + } /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2740,10 +2776,15 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ check_stack_depth(); - if (jbv->type != jbvBinary || !JsonContainerIsArray(jbc)) + if (jbv->type != jbvBinary || + !JsonContainerIsArray(jbc) || + JsonContainerIsScalar(jbc)) + { populate_array_report_expected_array(ctx, ndim - 1); - - Assert(!JsonContainerIsScalar(jbc)); + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + return; + } it = JsonbIteratorInit(jbc); @@ -2762,7 +2803,12 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ (tok == WJB_ELEM && (val.type != jbvBinary || !JsonContainerIsArray(val.val.binary.data))))) + { populate_array_assign_ndims(ctx, ndim); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + } jsv.is_json = false; jsv.val.jsonb = &val; @@ -2780,6 +2826,9 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ { /* populate child sub-array */ populate_array_dim_jsonb(ctx, &val, ndim + 1); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2797,12 +2846,18 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ Assert(tok == WJB_DONE && !it); } -/* recursively populate an array from json/jsonb */ +/* + * Recursively populate an array from json/jsonb + * + * *isnull is set to true if an error is reported during parsing. + */ static Datum populate_array(ArrayIOData *aio, const char *colname, MemoryContext mcxt, - JsValue *jsv) + JsValue *jsv, + Node *escontext, + bool *isnull) { PopulateArrayContext ctx; Datum result; @@ -2817,6 +2872,7 @@ populate_array(ArrayIOData *aio, ctx.ndims = 0; /* unknown yet */ ctx.dims = NULL; ctx.sizes = NULL; + ctx.escontext = escontext; if (jsv->is_json) populate_array_json(&ctx, jsv->val.json.str, @@ -2825,7 +2881,16 @@ populate_array(ArrayIOData *aio, else { populate_array_dim_jsonb(&ctx, jsv->val.jsonb, 1); - ctx.dims[0] = ctx.sizes[0]; + /* Nothing to do on an error. */ + if (!SOFT_ERROR_OCCURRED(ctx.escontext)) + ctx.dims[0] = ctx.sizes[0]; + } + + /* Nothing to return if an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx.escontext)) + { + *isnull = true; + return (Datum) 0; } Assert(ctx.ndims > 0); @@ -2842,6 +2907,7 @@ populate_array(ArrayIOData *aio, pfree(ctx.sizes); pfree(lbs); + *isnull = false; return result; } @@ -2957,7 +3023,8 @@ populate_composite(CompositeIOData *io, /* populate non-null scalar value from json/jsonb value */ static Datum -populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) +populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext) { Datum res; char *str = NULL; @@ -3028,7 +3095,11 @@ populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) elog(ERROR, "unrecognized jsonb type: %d", (int) jbv->type); } - res = InputFunctionCall(&io->typiofunc, str, io->typioparam, typmod); + if (!InputFunctionCallSafe(&io->typiofunc, str, io->typioparam, typmod, + escontext, &res)) + { + res = (Datum) 0; + } /* free temporary buffer */ if (str != json) @@ -3054,7 +3125,7 @@ populate_domain(DomainIOData *io, res = populate_record_field(io->base_io, io->base_typid, io->base_typmod, colname, mcxt, PointerGetDatum(NULL), - jsv, &isnull); + jsv, &isnull, NULL); Assert(!isnull); } @@ -3159,7 +3230,8 @@ populate_record_field(ColumnIOData *col, MemoryContext mcxt, Datum defaultval, JsValue *jsv, - bool *isnull) + bool *isnull, + Node *escontext) { TypeCat typcat; @@ -3192,10 +3264,12 @@ populate_record_field(ColumnIOData *col, switch (typcat) { case TYPECAT_SCALAR: - return populate_scalar(&col->scalar_io, typid, typmod, jsv); + return populate_scalar(&col->scalar_io, typid, typmod, jsv, + escontext); case TYPECAT_ARRAY: - return populate_array(&col->io.array, colname, mcxt, jsv); + return populate_array(&col->io.array, colname, mcxt, jsv, + escontext, isnull); case TYPECAT_COMPOSITE: case TYPECAT_COMPOSITE_DOMAIN: @@ -3216,6 +3290,53 @@ populate_record_field(ColumnIOData *col, } } +/* recursively populate specified type from a json/jsonb value */ +Datum +json_populate_type(Datum json_val, Oid json_type, Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext) +{ + JsValue jsv = {0}; + JsonbValue jbv; + + jsv.is_json = json_type == JSONOID; + + if (*isnull) + { + if (jsv.is_json) + jsv.val.json.str = NULL; + else + jsv.val.jsonb = NULL; + } + else if (jsv.is_json) + { + text *json = DatumGetTextPP(json_val); + + jsv.val.json.str = VARDATA_ANY(json); + jsv.val.json.len = VARSIZE_ANY_EXHDR(json); + jsv.val.json.type = JSON_TOKEN_INVALID; /* not used in + * populate_composite() */ + } + else + { + Jsonb *jsonb = DatumGetJsonbP(json_val); + + jsv.val.jsonb = &jbv; + + /* fill binary jsonb value pointing to jb */ + jbv.type = jbvBinary; + jbv.val.binary.data = &jsonb->root; + jbv.val.binary.len = VARSIZE(jsonb) - VARHDRSZ; + } + + if (!*cache) + *cache = MemoryContextAllocZero(mcxt, sizeof(ColumnIOData)); + + return populate_record_field(*cache, typid, typmod, NULL, mcxt, + PointerGetDatum(NULL), &jsv, isnull, + escontext); +} + static RecordIOData * allocate_record_info(MemoryContext mcxt, int ncolumns) { @@ -3357,7 +3478,8 @@ populate_record(TupleDesc tupdesc, mcxt, nulls[i] ? (Datum) 0 : values[i], &field, - &nulls[i]); + &nulls[i], + NULL); } res = heap_form_tuple(tupdesc, values, nulls); diff --git a/src/backend/utils/adt/jsonpath.c b/src/backend/utils/adt/jsonpath.c index 7891fde310..5194d0d91f 100644 --- a/src/backend/utils/adt/jsonpath.c +++ b/src/backend/utils/adt/jsonpath.c @@ -68,7 +68,9 @@ #include "libpq/pqformat.h" #include "nodes/miscnodes.h" #include "miscadmin.h" +#include "nodes/nodeFuncs.h" #include "utils/builtins.h" +#include "utils/formatting.h" #include "utils/json.h" #include "utils/jsonpath.h" @@ -1109,3 +1111,256 @@ jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, return true; } + +/* SQL/JSON datatype status: */ +typedef enum JsonPathDatatypeStatus +{ + jpdsNonDateTime, /* null, bool, numeric, string, array, object */ + jpdsUnknownDateTime, /* unknown datetime type */ + jpdsDateTimeZoned, /* timetz, timestamptz */ + jpdsDateTimeNonZoned /* time, timestamp, date */ +} JsonPathDatatypeStatus; + +/* Context for jspIsMutableWalker() */ +typedef struct JsonPathMutableContext +{ + List *varnames; /* list of variable names */ + List *varexprs; /* list of variable expressions */ + JsonPathDatatypeStatus current; /* status of @ item */ + bool lax; /* jsonpath is lax or strict */ + bool mutable; /* resulting mutability status */ +} JsonPathMutableContext; + +/* + * Recursive walker for jspIsMutable() + */ +static JsonPathDatatypeStatus +jspIsMutableWalker(JsonPathItem *jpi, JsonPathMutableContext *cxt) +{ + JsonPathItem next; + JsonPathDatatypeStatus status = jpdsNonDateTime; + + while (!cxt->mutable) + { + JsonPathItem arg; + JsonPathDatatypeStatus leftStatus; + JsonPathDatatypeStatus rightStatus; + + switch (jpi->type) + { + case jpiRoot: + Assert(status == jpdsNonDateTime); + break; + + case jpiCurrent: + Assert(status == jpdsNonDateTime); + status = cxt->current; + break; + + case jpiFilter: + { + JsonPathDatatypeStatus prevStatus = cxt->current; + + cxt->current = status; + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + + cxt->current = prevStatus; + break; + } + + case jpiVariable: + { + int32 len; + const char *name = jspGetString(jpi, &len); + ListCell *lc1; + ListCell *lc2; + + Assert(status == jpdsNonDateTime); + + forboth(lc1, cxt->varnames, lc2, cxt->varexprs) + { + String *varname = lfirst_node(String, lc1); + Node *varexpr = lfirst(lc2); + + if (strncmp(varname->sval, name, len)) + continue; + + switch (exprType(varexpr)) + { + case DATEOID: + case TIMEOID: + case TIMESTAMPOID: + status = jpdsDateTimeNonZoned; + break; + + case TIMETZOID: + case TIMESTAMPTZOID: + status = jpdsDateTimeZoned; + break; + + default: + status = jpdsNonDateTime; + break; + } + + break; + } + break; + } + + case jpiEqual: + case jpiNotEqual: + case jpiLess: + case jpiGreater: + case jpiLessOrEqual: + case jpiGreaterOrEqual: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + leftStatus = jspIsMutableWalker(&arg, cxt); + + jspGetRightArg(jpi, &arg); + rightStatus = jspIsMutableWalker(&arg, cxt); + + /* + * Comparison of datetime type with different timezone status + * is mutable. + */ + if (leftStatus != jpdsNonDateTime && + rightStatus != jpdsNonDateTime && + (leftStatus == jpdsUnknownDateTime || + rightStatus == jpdsUnknownDateTime || + leftStatus != rightStatus)) + cxt->mutable = true; + break; + + case jpiNot: + case jpiIsUnknown: + case jpiExists: + case jpiPlus: + case jpiMinus: + Assert(status == jpdsNonDateTime); + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiAnd: + case jpiOr: + case jpiAdd: + case jpiSub: + case jpiMul: + case jpiDiv: + case jpiMod: + case jpiStartsWith: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + jspGetRightArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiIndexArray: + for (int i = 0; i < jpi->content.array.nelems; i++) + { + JsonPathItem from; + JsonPathItem to; + + if (jspGetArraySubscript(jpi, &from, &to, i)) + jspIsMutableWalker(&to, cxt); + + jspIsMutableWalker(&from, cxt); + } + /* FALLTHROUGH */ + + case jpiAnyArray: + if (!cxt->lax) + status = jpdsNonDateTime; + break; + + case jpiAny: + if (jpi->content.anybounds.first > 0) + status = jpdsNonDateTime; + break; + + case jpiDatetime: + if (jpi->content.arg) + { + char *template; + + jspGetArg(jpi, &arg); + if (arg.type != jpiString) + { + status = jpdsNonDateTime; + break; /* there will be runtime error */ + } + + template = jspGetString(&arg, NULL); + if (datetime_format_has_tz(template)) + status = jpdsDateTimeZoned; + else + status = jpdsDateTimeNonZoned; + } + else + { + status = jpdsUnknownDateTime; + } + break; + + case jpiLikeRegex: + Assert(status == jpdsNonDateTime); + jspInitByBuffer(&arg, jpi->base, jpi->content.like_regex.expr); + jspIsMutableWalker(&arg, cxt); + break; + + /* literals */ + case jpiNull: + case jpiString: + case jpiNumeric: + case jpiBool: + /* accessors */ + case jpiKey: + case jpiAnyKey: + /* special items */ + case jpiSubscript: + case jpiLast: + /* item methods */ + case jpiType: + case jpiSize: + case jpiAbs: + case jpiFloor: + case jpiCeiling: + case jpiDouble: + case jpiKeyValue: + status = jpdsNonDateTime; + break; + } + + if (!jspGetNext(jpi, &next)) + break; + + jpi = &next; + } + + return status; +} + +/* + * Check whether jsonpath expression is immutable or not. + */ +bool +jspIsMutable(JsonPath *path, List *varnames, List *varexprs) +{ + JsonPathMutableContext cxt; + JsonPathItem jpi; + + cxt.varnames = varnames; + cxt.varexprs = varexprs; + cxt.current = jpdsNonDateTime; + cxt.lax = (path->header & JSONPATH_LAX) != 0; + cxt.mutable = false; + + jspInit(&jpi, path); + jspIsMutableWalker(&jpi, &cxt); + + return cxt.mutable; +} diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index 2d0599b4aa..eded22e194 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -87,12 +87,16 @@ typedef struct JsonBaseObjectInfo int id; } JsonBaseObjectInfo; +typedef int (*JsonPathVarCallback) (void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); + /* * Context of jsonpath execution. */ typedef struct JsonPathExecContext { - Jsonb *vars; /* variables to substitute into jsonpath */ + void *vars; /* variables to substitute into jsonpath */ + JsonPathVarCallback getVar; JsonbValue *root; /* for $ evaluation */ JsonbValue *current; /* for @ evaluation */ JsonBaseObjectInfo baseObject; /* "base object" for .keyvalue() @@ -174,7 +178,8 @@ typedef JsonPathBool (*JsonPathPredicateCallback) (JsonPathItem *jsp, void *param); typedef Numeric (*BinaryArithmFunc) (Numeric num1, Numeric num2, bool *error); -static JsonPathExecResult executeJsonPath(JsonPath *path, Jsonb *vars, +static JsonPathExecResult executeJsonPath(JsonPath *path, void *vars, + JsonPathVarCallback getVar, Jsonb *json, bool throwErrors, JsonValueList *result, bool useTz); static JsonPathExecResult executeItem(JsonPathExecContext *cxt, @@ -225,8 +230,13 @@ static JsonPathExecResult appendBoolResult(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonValueList *found, JsonPathBool res); static void getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, JsonbValue *value); +static int GetJsonPathVar(void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); static void getJsonPathVariable(JsonPathExecContext *cxt, - JsonPathItem *variable, Jsonb *vars, JsonbValue *value); + JsonPathItem *variable, JsonbValue *value); +static int getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, + int varNameLen, JsonbValue *val, + JsonbValue *baseObject); static int JsonbArraySize(JsonbValue *jb); static JsonPathBool executeComparison(JsonPathItem *cmp, JsonbValue *lv, JsonbValue *rv, void *p); @@ -284,7 +294,8 @@ jsonb_path_exists_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - res = executeJsonPath(jp, vars, jb, !silent, NULL, tz); + res = executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, NULL, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -339,7 +350,8 @@ jsonb_path_match_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -417,7 +429,8 @@ jsonb_path_query_internal(FunctionCallInfo fcinfo, bool tz) vars = PG_GETARG_JSONB_P_COPY(2); silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); funcctx->user_fctx = JsonValueListGetList(&found); @@ -464,7 +477,8 @@ jsonb_path_query_array_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_RETURN_JSONB_P(JsonbValueToJsonb(wrapItemsInArray(&found))); } @@ -495,7 +509,8 @@ jsonb_path_query_first_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); if (JsonValueListLength(&found) >= 1) PG_RETURN_JSONB_P(JsonbValueToJsonb(JsonValueListHead(&found))); @@ -537,8 +552,9 @@ jsonb_path_query_first_tz(PG_FUNCTION_ARGS) * In other case it tries to find all the satisfied result items. */ static JsonPathExecResult -executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, - JsonValueList *result, bool useTz) +executeJsonPath(JsonPath *path, void *vars, JsonPathVarCallback getVar, + Jsonb *json, bool throwErrors, JsonValueList *result, + bool useTz) { JsonPathExecContext cxt; JsonPathExecResult res; @@ -550,22 +566,16 @@ executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, if (!JsonbExtractScalar(&json->root, &jbv)) JsonbInitBinary(&jbv, json); - if (vars && !JsonContainerIsObject(&vars->root)) - { - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"vars\" argument is not an object"), - errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); - } - cxt.vars = vars; + cxt.getVar = getVar; cxt.laxMode = (path->header & JSONPATH_LAX) != 0; cxt.ignoreStructuralErrors = cxt.laxMode; cxt.root = &jbv; cxt.current = &jbv; cxt.baseObject.jbc = NULL; cxt.baseObject.id = 0; - cxt.lastGeneratedObjectId = vars ? 2 : 1; + /* 1 + number of base objects in vars */ + cxt.lastGeneratedObjectId = 1 + getVar(vars, NULL, 0, NULL, NULL); cxt.innermostArraySize = -1; cxt.throwErrors = throwErrors; cxt.useTz = useTz; @@ -2108,54 +2118,118 @@ getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, &value->val.string.len); break; case jpiVariable: - getJsonPathVariable(cxt, item, cxt->vars, value); + getJsonPathVariable(cxt, item, value); return; default: elog(ERROR, "unexpected jsonpath item type"); } } +/* + * Returns the computed value of a JSON path variable with given name. + */ +static int +GetJsonPathVar(void *cxt, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject) +{ + JsonPathVariable *var = NULL; + List *vars = cxt; + ListCell *lc; + int id = 1; + + if (!varName) + return list_length(vars); + + foreach(lc, vars) + { + JsonPathVariable *curvar = lfirst(lc); + + if (!strncmp(curvar->name, varName, varNameLen)) + { + var = curvar; + break; + } + + id++; + } + + if (!var) + return -1; + + if (var->isnull) + { + val->type = jbvNull; + return 0; + } + + JsonItemFromDatum(var->value, var->typid, var->typmod, val); + + *baseObject = *val; + return id; +} + /* * Get the value of variable passed to jsonpath executor */ static void getJsonPathVariable(JsonPathExecContext *cxt, JsonPathItem *variable, - Jsonb *vars, JsonbValue *value) + JsonbValue *value) { char *varName; int varNameLength; + JsonbValue baseObject; + int baseObjectId; + + Assert(variable->type == jpiVariable); + varName = jspGetString(variable, &varNameLength); + + if (!cxt->vars || + (baseObjectId = cxt->getVar(cxt->vars, varName, varNameLength, value, + &baseObject)) < 0) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("could not find jsonpath variable \"%s\"", + pnstrdup(varName, varNameLength)))); + + if (baseObjectId > 0) + setBaseObject(cxt, &baseObject, baseObjectId); +} + +static int +getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, int varNameLength, + JsonbValue *value, JsonbValue *baseObject) +{ + Jsonb *vars = varsJsonb; JsonbValue tmp; JsonbValue *v; - if (!vars) + if (!varName) { - value->type = jbvNull; - return; + if (vars && !JsonContainerIsObject(&vars->root)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"vars\" argument is not an object"), + errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); + } + + return vars ? 1 : 0; /* count of base objects */ } - Assert(variable->type == jpiVariable); - varName = jspGetString(variable, &varNameLength); tmp.type = jbvString; tmp.val.string.val = varName; tmp.val.string.len = varNameLength; v = findJsonbValueFromContainer(&vars->root, JB_FOBJECT, &tmp); - if (v) - { - *value = *v; - pfree(v); - } - else - { - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("could not find jsonpath variable \"%s\"", - pnstrdup(varName, varNameLength)))); - } + if (!v) + return -1; - JsonbInitBinary(&tmp, vars); - setBaseObject(cxt, &tmp, 1); + *value = *v; + pfree(v); + + JsonbInitBinary(baseObject, vars); + return 1; } /**************** Support functions for JsonPath execution *****************/ @@ -2812,3 +2886,240 @@ compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, return DatumGetInt32(DirectFunctionCall2(cmpfunc, val1, val2)); } + +/* Executor-callable JSON_EXISTS implementation */ +bool +JsonPathExists(Datum jb, JsonPath *jp, List *vars, bool *error) +{ + JsonPathExecResult res = executeJsonPath(jp, vars, GetJsonPathVar, + DatumGetJsonbP(jb), !error, NULL, + true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + *error = true; + + return res == jperOk; +} + +/* Executor-callable JSON_QUERY implementation */ +Datum +JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, bool *empty, + bool *error, List *vars) +{ + JsonbValue *first; + bool wrap; + JsonValueList found = {0}; + JsonPathExecResult res PG_USED_FOR_ASSERTS_ONLY; + int count; + + res = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + { + *error = true; + *empty = false; + return (Datum) 0; + } + + count = JsonValueListLength(&found); + + first = count ? JsonValueListHead(&found) : NULL; + + if (!first) + wrap = false; + else if (wrapper == JSW_NONE) + wrap = false; + else if (wrapper == JSW_UNCONDITIONAL) + wrap = true; + else if (wrapper == JSW_CONDITIONAL) + wrap = count > 1 || + IsAJsonbScalar(first) || + (first->type == jbvBinary && + JsonContainerIsScalar(first->val.binary.data)); + else + { + elog(ERROR, "unrecognized json wrapper %d", wrapper); + wrap = false; + } + + if (wrap) + return JsonbPGetDatum(JsonbValueToJsonb(wrapItemsInArray(&found))); + + if (count > 1) + { + if (error) + { + *error = true; + return (Datum) 0; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_QUERY should return singleton item without wrapper"), + errhint("Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array."))); + } + + if (first) + return JsonbPGetDatum(JsonbValueToJsonb(first)); + + *empty = true; + return PointerGetDatum(NULL); +} + +/* Executor-callable JSON_VALUE implementation */ +JsonbValue * +JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars) +{ + JsonbValue *res; + JsonValueList found = {0}; + JsonPathExecResult jper PG_USED_FOR_ASSERTS_ONLY; + int count; + + jper = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(jper)); + + if (error && jperIsError(jper)) + { + *error = true; + *empty = false; + return NULL; + } + + count = JsonValueListLength(&found); + + *empty = !count; + + if (*empty) + return NULL; + + if (count > 1) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + res = JsonValueListHead(&found); + + if (res->type == jbvBinary && + JsonContainerIsScalar(res->val.binary.data)) + JsonbExtractScalar(res->val.binary.data, res); + + if (!IsAJsonbScalar(res)) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_SCALAR_REQUIRED), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + if (res->type == jbvNull) + return NULL; + + return res; +} + +static void +JsonbValueInitNumericDatum(JsonbValue *jbv, Datum num) +{ + jbv->type = jbvNumeric; + jbv->val.numeric = DatumGetNumeric(num); +} + +void +JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) +{ + switch (typid) + { + case BOOLOID: + res->type = jbvBool; + res->val.boolean = DatumGetBool(val); + break; + case NUMERICOID: + JsonbValueInitNumericDatum(res, val); + break; + case INT2OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int2_numeric, val)); + break; + case INT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int4_numeric, val)); + break; + case INT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int8_numeric, val)); + break; + case FLOAT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float4_numeric, val)); + break; + case FLOAT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float8_numeric, val)); + break; + case TEXTOID: + case VARCHAROID: + res->type = jbvString; + res->val.string.val = VARDATA_ANY(val); + res->val.string.len = VARSIZE_ANY_EXHDR(val); + break; + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + res->type = jbvDatetime; + res->val.datetime.value = val; + res->val.datetime.typid = typid; + res->val.datetime.typmod = typmod; + res->val.datetime.tz = 0; + break; + case JSONBOID: + { + JsonbValue *jbv = res; + Jsonb *jb = DatumGetJsonbP(val); + + if (JsonContainerIsScalar(&jb->root)) + { + bool result PG_USED_FOR_ASSERTS_ONLY; + + result = JsonbExtractScalar(&jb->root, jbv); + Assert(result); + } + else + JsonbInitBinary(jbv, jb); + break; + } + case JSONOID: + { + text *txt = DatumGetTextP(val); + char *str = text_to_cstring(txt); + Jsonb *jb; + + jb = DatumGetJsonbP(DirectFunctionCall1(jsonb_in, + CStringGetDatum(str))); + pfree(str); + + JsonItemFromDatum(JsonbPGetDatum(jb), JSONBOID, -1, res); + break; + } + default: + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); + } +} diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 4ef9173c0b..a23e1f7af5 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -476,6 +476,8 @@ static void get_const_expr(Const *constval, deparse_context *context, int showtype); static void get_const_collation(Const *constval, deparse_context *context); static void get_json_format(JsonFormat *format, StringInfo buf); +static void get_json_returning(JsonReturning *returning, StringInfo buf, + bool json_format_by_default); static void get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, bool showimplicit); static void get_json_constructor_options(JsonConstructorExpr *ctor, @@ -518,6 +520,8 @@ static char *generate_qualified_type_name(Oid typid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); +static void get_json_path_spec(Node *path_spec, deparse_context *context, + bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8279,6 +8283,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_WindowFunc: case T_FuncExpr: case T_JsonConstructorExpr: + case T_JsonExpr: /* function-like: name(..) or name[..] */ return true; @@ -8450,6 +8455,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_GroupingFunc: /* own parentheses */ case T_WindowFunc: /* own parentheses */ case T_CaseExpr: /* other separators */ + case T_JsonExpr: /* own parentheses */ return true; default: return false; @@ -8565,6 +8571,65 @@ get_rule_expr_paren(Node *node, deparse_context *context, appendStringInfoChar(context->buf, ')'); } +static void +get_json_behavior(JsonBehavior *behavior, deparse_context *context, + const char *on) +{ + /* + * The order of array elements must correspond to the order of + * JsonBehaviorType members. + */ + const char *behavior_names[] = + { + " NULL", + " ERROR", + " EMPTY", + " TRUE", + " FALSE", + " UNKNOWN", + " EMPTY ARRAY", + " EMPTY OBJECT", + " DEFAULT " + }; + + if ((int) behavior->btype < 0 || behavior->btype >= lengthof(behavior_names)) + elog(ERROR, "invalid json behavior type: %d", behavior->btype); + + appendStringInfoString(context->buf, behavior_names[behavior->btype]); + + if (behavior->btype == JSON_BEHAVIOR_DEFAULT) + get_rule_expr(behavior->default_expr, context, false); + + appendStringInfo(context->buf, " ON %s", on); +} + +/* + * get_json_expr_options + * + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + */ +static void +get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, + JsonBehaviorType default_behavior) +{ + if (jsexpr->op == JSON_QUERY_OP) + { + if (jsexpr->wrapper == JSW_CONDITIONAL) + appendStringInfo(context->buf, " WITH CONDITIONAL WRAPPER"); + else if (jsexpr->wrapper == JSW_UNCONDITIONAL) + appendStringInfo(context->buf, " WITH UNCONDITIONAL WRAPPER"); + + if (jsexpr->omit_quotes) + appendStringInfo(context->buf, " OMIT QUOTES"); + } + + if (jsexpr->op != JSON_EXISTS_OP && + jsexpr->on_empty->btype != default_behavior) + get_json_behavior(jsexpr->on_empty, context, "EMPTY"); + + if (jsexpr->on_error->btype != default_behavior) + get_json_behavior(jsexpr->on_error, context, "ERROR"); +} /* ---------- * get_rule_expr - Parse back an expression @@ -9724,6 +9789,7 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonValueExpr: { JsonValueExpr *jve = (JsonValueExpr *) node; @@ -9773,6 +9839,63 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + appendStringInfoString(buf, "JSON_QUERY("); + break; + case JSON_VALUE_OP: + appendStringInfoString(buf, "JSON_VALUE("); + break; + case JSON_EXISTS_OP: + appendStringInfoString(buf, "JSON_EXISTS("); + break; + } + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_json_path_spec(jexpr->path_spec, context, showimplicit); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoString(buf, " PASSING "); + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + get_rule_expr((Node *) lfirst(lc2), context, showimplicit); + appendStringInfo(buf, " AS %s", + ((String *) lfirst_node(String, lc1))->sval); + } + } + + if (jexpr->op != JSON_EXISTS_OP || + jexpr->returning->typid != BOOLOID) + get_json_returning(jexpr->returning, context->buf, + jexpr->op == JSON_QUERY_OP); + + get_json_expr_options(jexpr, context, + jexpr->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : JSON_BEHAVIOR_NULL); + + appendStringInfoString(buf, ")"); + } + break; + case T_List: { char *sep; @@ -9896,6 +10019,7 @@ looks_like_function(Node *node) case T_MinMaxExpr: case T_SQLValueFunction: case T_XmlExpr: + case T_JsonExpr: /* these are all accepted by func_expr_common_subexpr */ return true; default: @@ -10755,6 +10879,18 @@ get_const_collation(Const *constval, deparse_context *context) } } +/* + * get_json_path_spec - Parse back a JSON path specification + */ +static void +get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit) +{ + if (IsA(path_spec, Const)) + get_const_expr((Const *) path_spec, context, -1); + else + get_rule_expr(path_spec, context, showimplicit); +} + /* * get_json_format - Parse back a JsonFormat node */ diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index 048573c2bc..69fb577850 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -16,12 +16,15 @@ #include "executor/nodeAgg.h" #include "nodes/execnodes.h" +#include "nodes/miscnodes.h" /* forward references to avoid circularity */ struct ExprEvalStep; struct SubscriptingRefState; struct ScalarArrayOpExprHashTable; struct JsonConstructorExprState; +struct JsonbValue; +struct JsonExprState; /* Bits in ExprState->flags (see also execnodes.h for public flag bits): */ /* expression's interpreter has been initialized */ @@ -238,6 +241,11 @@ typedef enum ExprEvalOp EEOP_XMLEXPR, EEOP_JSON_CONSTRUCTOR, EEOP_IS_JSON, + EEOP_JSONEXPR_SKIP, + EEOP_JSONEXPR_PATH, + EEOP_JSONEXPR_BEHAVIOR, + EEOP_JSONEXPR_COERCION, + EEOP_JSONEXPR_COERCION_FINISH, EEOP_AGGREF, EEOP_GROUPING_FUNC, EEOP_WINDOW_FUNC, @@ -689,6 +697,57 @@ typedef struct ExprEvalStep JsonIsPredicate *pred; /* original expression node */ } is_json; + /* for EEOP_JSONEXPR_PATH */ + struct + { + struct JsonExprState *jsestate; + } jsonexpr; + + /* for EEOP_JSONEXPR_SKIP */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprSkip() */ + int jump_coercion; + int jump_passing_args; + } jsonexpr_skip; + + /* for EEOP_JSONEXPR_BEHAVIOR */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprBehavior() */ + int jump_onerror_expr; + int jump_onempty_expr; + int jump_coercion; + int jump_skip_coercion; + } jsonexpr_behavior; + + /* for EEOP_JSONEXPR_COERCION */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion; + + /* for EEOP_JSONEXPR_COERCION_FINISH */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion_finish; } d; } ExprEvalStep; @@ -752,6 +811,111 @@ typedef struct JsonConstructorExprState int nargs; } JsonConstructorExprState; +/* + * Information computed before evaluating a JsonExpr expression. + */ +typedef struct JsonExprPreEvalState +{ + /* value/isnull for JsonExpr.formatted_expr */ + NullableDatum formatted_expr; + + /* value/isnull for JsonExpr.pathspec */ + NullableDatum pathspec; + + /* JsonPathVariable entries for JsonExpr.passing_values */ + List *args; +} JsonExprPreEvalState; + +/* + * State for a given member of JsonItemCoercions. + */ +typedef struct JsonCoercionState +{ + /* Expression used to evaluate the coercion */ + JsonCoercion *coercion; + + /* ExprEvalStep to compute this coercion's expression */ + int jump_eval_expr; +} JsonCoercionState; + +/* + * State for evaluating the coercion for a given JSON item using one of + * the following coercions. + * + * Note that while ExecInitExprRec() for JsonItemCoercions will initialize + * ExprEvalSteps for all of the members that need it, only one will get run + * during a given evaluation of the enclosing JsonExpr depending on the type + * of the result JSON item. + */ +typedef struct JsonItemCoercionsState +{ + JsonCoercionState *null; + JsonCoercionState *string; + JsonCoercionState *numeric; + JsonCoercionState *boolean; + JsonCoercionState *date; + JsonCoercionState *time; + JsonCoercionState *timetz; + JsonCoercionState *timestamp; + JsonCoercionState *timestamptz; + JsonCoercionState *composite; +} JsonItemCoercionsState; + +/* + * State for some post-JsonExpr-evaluation processing steps that gets filled + * in JsonExpr evaluation. + */ +typedef struct JsonExprPostEvalState +{ + /* Is JSON item empty? */ + bool empty; + + /* Did JSON item evaluation cause an error? */ + bool error; + + /* Cache for json_populate_type() called for coercion in some cases */ + void *cache; + + /* + * State for evaluating a JSON item coercion. Points to one of those in + * JsonExprState.coercions chosen by ExecPrepareJsonItemCoercion(). + */ + JsonCoercionState *item_jcstate; + bool coercion_error; + bool coercion_done; + + ErrorSaveContext escontext; +} JsonExprPostEvalState; + +/* EEOP_JSONEXPR state, too big to inline */ +typedef struct JsonExprState +{ + /* original expression node */ + JsonExpr *jsexpr; + + /* + * Should errors be thrown or handled softly? Different parts of + * evaluating a given JsonExpr may require different treatment depending + * on the JsonExprOp; see ExecEvalJsonExprBehavior(). + */ + bool throw_error; + + JsonExprPreEvalState pre_eval; + JsonExprPostEvalState post_eval; + + struct + { + FmgrInfo *finfo; /* typinput function for output type */ + Oid typioparam; + } input; /* I/O info for output type */ + + /* + * Either of the following two is used by ExecEvalJsonExprCoercion() to + * apply coercion to the final result if needed. + */ + JsonCoercionState *result_jcstate; + JsonItemCoercionsState *item_jcstates; +} JsonExprState; /* functions in execExpr.c */ extern void ExprEvalPushStep(ExprState *es, const ExprEvalStep *s); @@ -805,6 +969,14 @@ extern void ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op); extern void ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull); +extern int ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op); +extern void ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, + ExprContext *econtext); extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op); extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op, ExprContext *econtext); diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index c677e490d7..709ad967ba 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -17,6 +17,7 @@ #include "executor/execdesc.h" #include "fmgr.h" #include "nodes/lockoptions.h" +#include "nodes/miscnodes.h" #include "nodes/parsenodes.h" #include "utils/memutils.h" diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index cb714f4a19..584bf7001a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -129,6 +129,9 @@ typedef struct ExprState Datum *innermost_domainval; bool *innermost_domainnull; + + /* ErrorSaveContext for soft-error capture. */ + Node *escontext; } ExprState; diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 3180703005..c1d437b44e 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -112,6 +112,7 @@ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *raw_expr, Expr *formatted_expr, JsonFormat *format); +extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index d926713bd9..7d63a37d5f 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1727,6 +1727,12 @@ typedef enum JsonQuotes JS_QUOTES_OMIT /* OMIT QUOTES */ } JsonQuotes; +/* + * JsonPathSpec - + * representation of JSON path constant + */ +typedef char *JsonPathSpec; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1738,6 +1744,48 @@ typedef struct JsonOutput JsonReturning *returning; /* RETURNING FORMAT clause and type Oids */ } JsonOutput; +/* + * JsonArgument - + * representation of argument from JSON PASSING clause + */ +typedef struct JsonArgument +{ + NodeTag type; + JsonValueExpr *val; /* argument value expression */ + char *name; /* argument name */ +} JsonArgument; + +/* + * JsonCommon - + * representation of common syntax of functions using JSON path + */ +typedef struct JsonCommon +{ + NodeTag type; + JsonValueExpr *expr; /* context item expression */ + Node *pathspec; /* JSON path specification expression */ + char *pathname; /* path name, if any */ + List *passing; /* list of PASSING clause arguments, if any */ + int location; /* token location, or -1 if unknown */ +} JsonCommon; + +/* + * JsonFuncExpr - + * untransformed representation of JSON function expressions + */ +typedef struct JsonFuncExpr +{ + NodeTag type; + JsonExprOp op; /* expression type */ + JsonCommon *common; /* common syntax */ + JsonOutput *output; /* output clause, if specified */ + JsonBehavior *on_empty; /* ON EMPTY behavior, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + JsonWrapper wrapper; /* array wrapper behavior (JSON_QUERY only) */ + bool omit_quotes; /* omit or keep quotes? (JSON_QUERY only) */ + int location; /* token location, or -1 if unknown */ +} JsonFuncExpr; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 4104371bd9..64fdbc57fa 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1544,6 +1544,17 @@ typedef struct XmlExpr int location; } XmlExpr; +/* + * JsonExprOp - + * enumeration of JSON functions using JSON path + */ +typedef enum JsonExprOp +{ + JSON_VALUE_OP, /* JSON_VALUE() */ + JSON_QUERY_OP, /* JSON_QUERY() */ + JSON_EXISTS_OP /* JSON_EXISTS() */ +} JsonExprOp; + /* * JsonEncoding - * representation of JSON ENCODING clause @@ -1568,6 +1579,37 @@ typedef enum JsonFormatType * jsonb */ } JsonFormatType; +/* + * JsonBehaviorType - + * enumeration of behavior types used in JSON ON ... BEHAVIOR clause + * + * If enum members are reordered, get_json_behavior() from ruleutils.c + * must be updated accordingly. + */ +typedef enum JsonBehaviorType +{ + JSON_BEHAVIOR_NULL = 0, + JSON_BEHAVIOR_ERROR, + JSON_BEHAVIOR_EMPTY, + JSON_BEHAVIOR_TRUE, + JSON_BEHAVIOR_FALSE, + JSON_BEHAVIOR_UNKNOWN, + JSON_BEHAVIOR_EMPTY_ARRAY, + JSON_BEHAVIOR_EMPTY_OBJECT, + JSON_BEHAVIOR_DEFAULT +} JsonBehaviorType; + +/* + * JsonWrapper - + * representation of WRAPPER clause for JSON_QUERY() + */ +typedef enum JsonWrapper +{ + JSW_NONE, + JSW_CONDITIONAL, + JSW_UNCONDITIONAL, +} JsonWrapper; + /* * JsonFormat - * representation of JSON FORMAT clause @@ -1662,6 +1704,73 @@ typedef struct JsonIsPredicate int location; /* token location, or -1 if unknown */ } JsonIsPredicate; +/* + * JsonBehavior - + * representation of JSON ON ... BEHAVIOR clause + */ +typedef struct JsonBehavior +{ + NodeTag type; + JsonBehaviorType btype; /* behavior type */ + Node *default_expr; /* default expression, if any */ +} JsonBehavior; + +/* + * JsonCoercion - + * coercion from SQL/JSON item types to SQL types + */ +typedef struct JsonCoercion +{ + NodeTag type; + Node *expr; /* resulting expression coerced to target type */ + bool via_populate; /* coerce result using json_populate_type()? */ + bool via_io; /* coerce result using type input function? */ + Oid collation; /* collation for coercion via I/O or populate */ +} JsonCoercion; + +/* + * JsonItemCoercions - + * expressions for coercion from SQL/JSON item types directly to the + * output SQL type + */ +typedef struct JsonItemCoercions +{ + NodeTag type; + JsonCoercion *null; + JsonCoercion *string; + JsonCoercion *numeric; + JsonCoercion *boolean; + JsonCoercion *date; + JsonCoercion *time; + JsonCoercion *timetz; + JsonCoercion *timestamp; + JsonCoercion *timestamptz; + JsonCoercion *composite; /* arrays and objects */ +} JsonItemCoercions; + +/* + * JsonExpr - + * transformed representation of JSON_VALUE(), JSON_QUERY(), JSON_EXISTS() + */ +typedef struct JsonExpr +{ + Expr xpr; + JsonExprOp op; /* json function ID */ + Node *formatted_expr; /* formatted context item expression */ + JsonCoercion *result_coercion; /* resulting coercion to RETURNING type */ + JsonFormat *format; /* context item format (JSON/JSONB) */ + Node *path_spec; /* JSON path specification expression */ + List *passing_names; /* PASSING argument names */ + List *passing_values; /* PASSING argument values */ + JsonReturning *returning; /* RETURNING clause type/format info */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + JsonItemCoercions *coercions; /* coercions for JSON_VALUE */ + JsonWrapper wrapper; /* WRAPPER for JSON_QUERY */ + bool omit_quotes; /* KEEP/OMIT QUOTES for JSON_QUERY */ + int location; /* token location, or -1 if unknown */ +} JsonExpr; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 5984dcfa4b..0954d9fc7b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -93,6 +93,7 @@ PG_KEYWORD("commit", COMMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("committed", COMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("compression", COMPRESSION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("concurrently", CONCURRENTLY, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("conditional", CONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("configuration", CONFIGURATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("conflict", CONFLICT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("connection", CONNECTION, UNRESERVED_KEYWORD, BARE_LABEL) @@ -147,11 +148,13 @@ PG_KEYWORD("double", DOUBLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("drop", DROP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("each", EACH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("empty", EMPTY_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("event", EVENT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("except", EXCEPT, RESERVED_KEYWORD, AS_LABEL) @@ -233,10 +236,14 @@ PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_exists", JSON_EXISTS, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) @@ -302,6 +309,7 @@ PG_KEYWORD("off", OFF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("offset", OFFSET, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("oids", OIDS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("old", OLD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("omit", OMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("on", ON, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("only", ONLY, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("operator", OPERATOR, UNRESERVED_KEYWORD, BARE_LABEL) @@ -344,6 +352,7 @@ PG_KEYWORD("procedures", PROCEDURES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("publication", PUBLICATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("quotes", QUOTES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("range", RANGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("read", READ, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("real", REAL, COL_NAME_KEYWORD, BARE_LABEL) @@ -414,6 +423,7 @@ PG_KEYWORD("stdout", STDOUT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("storage", STORAGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("stored", STORED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("string", STRING_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("subscription", SUBSCRIPTION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("substring", SUBSTRING, COL_NAME_KEYWORD, BARE_LABEL) @@ -449,6 +459,7 @@ PG_KEYWORD("types", TYPES_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uescape", UESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unbounded", UNBOUNDED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uncommitted", UNCOMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("unconditional", UNCONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unencrypted", UNENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("union", UNION, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("unique", UNIQUE, RESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/formatting.h b/src/include/utils/formatting.h index 0cad3a2709..d4ca0f42ff 100644 --- a/src/include/utils/formatting.h +++ b/src/include/utils/formatting.h @@ -17,7 +17,6 @@ #ifndef _FORMATTING_H_ #define _FORMATTING_H_ - extern char *str_tolower(const char *buff, size_t nbytes, Oid collid); extern char *str_toupper(const char *buff, size_t nbytes, Oid collid); extern char *str_initcap(const char *buff, size_t nbytes, Oid collid); @@ -29,5 +28,6 @@ extern char *asc_initcap(const char *buff, size_t nbytes); extern Datum parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, Oid *typid, int32 *typmod, int *tz, struct Node *escontext); +extern bool datetime_format_has_tz(const char *fmt_str); #endif diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index f928e6142a..e628d4fdd0 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -422,6 +422,9 @@ extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, int estimated_len); +extern Jsonb *JsonbMakeEmptyArray(void); +extern Jsonb *JsonbMakeEmptyObject(void); +extern char *JsonbUnquote(Jsonb *jb); extern bool JsonbExtractScalar(JsonbContainer *jbc, JsonbValue *res); extern const char *JsonbTypeName(JsonbValue *val); diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index 5ee1964096..ff5dfffb49 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -15,6 +15,7 @@ #define JSONFUNCS_H #include "common/jsonapi.h" +#include "nodes/nodes.h" #include "utils/jsonb.h" /* @@ -86,5 +87,9 @@ extern Datum to_json_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid); extern Datum to_jsonb_worker(Datum val, JsonTypeCategory tcategory, Oid outfuncoid); +extern Datum json_populate_type(Datum json_val, Oid json_type, + Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext); #endif diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index f0181e045f..4dae78a98c 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -16,7 +16,9 @@ #include "fmgr.h" #include "nodes/pg_list.h" +#include "nodes/primnodes.h" #include "utils/jsonb.h" +#include "utils/jsonfuncs.h" typedef struct { @@ -184,6 +186,7 @@ extern bool jspGetBool(JsonPathItem *v); extern char *jspGetString(JsonPathItem *v, int32 *len); extern bool jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, int i); +extern bool jspIsMutable(JsonPath *path, List *varnames, List *varexprs); extern const char *jspOperationName(JsonPathItemType type); @@ -261,4 +264,28 @@ extern bool jspConvertRegexFlags(uint32 xflags, int *result, struct Node *escontext); +/* + * Evaluation of jsonpath + */ + +/* External variable passed into jsonpath. */ +typedef struct JsonPathVariable +{ + char *name; + Oid typid; + int32 typmod; + Datum value; + bool isnull; +} JsonPathVariable; + +/* SQL/JSON item */ +extern void JsonItemFromDatum(Datum val, Oid typid, int32 typmod, + JsonbValue *res); + +extern bool JsonPathExists(Datum jb, JsonPath *path, List *vars, bool *error); +extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, + bool *empty, bool *error, List *vars); +extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, + bool *error, List *vars); + #endif diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer index 435c139ec2..b2aa44f36d 100644 --- a/src/interfaces/ecpg/preproc/ecpg.trailer +++ b/src/interfaces/ecpg/preproc/ecpg.trailer @@ -651,6 +651,34 @@ var_type: simple_type $$.type_index = mm_strdup("-1"); $$.type_sizeof = NULL; } + | STRING_P + { + if (INFORMIX_MODE) + { + /* In Informix mode, "string" is automatically a typedef */ + $$.type_enum = ECPGt_string; + $$.type_str = mm_strdup("char"); + $$.type_dimension = mm_strdup("-1"); + $$.type_index = mm_strdup("-1"); + $$.type_sizeof = NULL; + } + else + { + /* Otherwise, legal only if user typedef'ed it */ + struct typedefs *this = get_typedef("string", false); + + $$.type_str = (this->type->type_enum == ECPGt_varchar || this->type->type_enum == ECPGt_bytea) ? EMPTY : mm_strdup(this->name); + $$.type_enum = this->type->type_enum; + $$.type_dimension = this->type->type_dimension; + $$.type_index = this->type->type_index; + if (this->type->type_sizeof && strlen(this->type->type_sizeof) != 0) + $$.type_sizeof = this->type->type_sizeof; + else + $$.type_sizeof = cat_str(3, mm_strdup("sizeof("), mm_strdup(this->name), mm_strdup(")")); + + struct_member_list[struct_level] = ECPGstruct_member_dup(this->struct_member_list); + } + } | INTERVAL ecpg_interval { $$.type_enum = ECPGt_interval; diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out new file mode 100644 index 0000000000..8b87580752 --- /dev/null +++ b/src/test/regress/expected/json_sqljson.out @@ -0,0 +1,18 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); +ERROR: JSON_EXISTS() is not yet implemented for the json type +LINE 1: SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_VALUE +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); +ERROR: JSON_VALUE() is not yet implemented for the json type +LINE 1: SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_QUERY +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); +ERROR: JSON_QUERY() is not yet implemented for the json type +LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out new file mode 100644 index 0000000000..22f8679917 --- /dev/null +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -0,0 +1,1042 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL::jsonb, '$'); + json_exists +------------- + +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb 'null', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_EXISTS(jsonb 'null', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{}', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + json_exists +------------- + f +(1 row) + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + json_exists +------------- + t +(1 row) + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); + json_exists +------------- + 1 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); + json_exists +------------- + 0 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); + json_exists +------------- + true +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +ERROR: cannot cast type boolean to jsonb +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +ERROR: cannot cast type boolean to real +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_EXISTS() +LINE 1: ...CT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSO... + ^ +-- JSON_VALUE +SELECT JSON_VALUE(NULL::jsonb, '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$'); + json_value +------------ + true +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + json_value +------------ + t +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); + json_value +------------ + 123 +(1 row) + +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); +ERROR: SQL/JSON item cannot be cast to target type +SELECT JSON_VALUE(jsonb '1.23', '$'); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "1.23" +SELECT JSON_VALUE(jsonb '"aaa"', '$'); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); + json_value +------------ + aa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "aaa" +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); + json_value +------------ + 111 +(1 row) + +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + ?column? +------------ + 03-01-2017 +(1 row) + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '[]', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '{}', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '1', '$.a'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); + json_value +------------ + error +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 3 +(1 row) + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); + json_value +------------ + 0 +(1 row) + +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: " " +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 5 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_VALUE() +LINE 1: ...CT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSO... + ^ +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + x | y +---+---- + 0 | -2 + 1 | 2 + 2 | -1 +(3 rows) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); + json_value +------------ + (1,2) +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + json_value +------------ + (1,2) +(1 row) + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); + json_value +-------------------------- + Tue Feb 20 18:34:56 2018 +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- JSON_QUERY +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + json_query | json_query | json_query | json_query | json_query +--------------------+--------------------+--------------------+----------------------+---------------------- + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, null, "2"] | [1, null, "2"] | [1, null, "2"] | [[1, null, "2"]] | [[1, null, "2"]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] +(6 rows) + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + unspec | without | with cond | with uncond | with +--------------------+--------------------+---------------------+----------------------+---------------------- + | | | | + | | | | + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, 2, 3] | [1, 2, 3] | [1, 2, 3] | [[1, 2, 3]] | [[1, 2, 3]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] + | | [1, "2", null, [3]] | [1, "2", null, [3]] | [1, "2", null, [3]] +(9 rows) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + json_query +------------ + \x616161 +(1 row) + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTE... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTE... + ^ +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_QUERY should return singleton item without wrapper +HINT: Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array. +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); + json_query +------------ + [1, +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); +ERROR: expected JSON array +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + x | y | list +---+---+-------------- + 0 | 0 | [] + 0 | 1 | [1] + 0 | 2 | [1, 2] + 0 | 3 | [1, 2, 3] + 0 | 4 | [1, 2, 3, 4] + 1 | 0 | [] + 1 | 1 | [1] + 1 | 2 | [1, 2] + 1 | 3 | [1, 2, 3] + 1 | 4 | [1, 2, 3, 4] + 2 | 0 | [] + 2 | 1 | [] + 2 | 2 | [2] + 2 | 3 | [2, 3] + 2 | 4 | [2, 3, 4] + 3 | 0 | [] + 3 | 1 | [] + 3 | 2 | [] + 3 | 3 | [3] + 3 | 4 | [3, 4] + 4 | 0 | [] + 4 | 1 | [] + 4 | 2 | [] + 4 | 3 | [] + 4 | 4 | [4] +(25 rows) + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); + json_query +----------------------------------------------------- + (1,aaa,"[1, ""2"", {}]","{""x"": [1, ""2"", {}]}",) +(1 row) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); + unnest +------------------------ + {"a": 1, "b": ["foo"]} + {"a": 2, "c": {}} + 123 +(3 rows) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); + json_query +-------------- + {1,2,NULL,3} +(1 row) + +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); + json_query +------------ + 1 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- Test constraints +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); +\d test_jsonb_constraints + Table "public.test_jsonb_constraints" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+-------------------------------------------------------------------------------- + js | text | | | + i | integer | | | + x | jsonb | | | JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +Check constraints: + "test_jsonb_constraint1" CHECK (js IS JSON) + "test_jsonb_constraint2" CHECK (JSON_EXISTS(js::jsonb, '$."a"' PASSING i + 5 AS int, i::text AS txt, ARRAY[1, 2, 3] AS arr)) + "test_jsonb_constraint3" CHECK (JSON_VALUE(js::jsonb, '$."a"' RETURNING integer DEFAULT ('12'::text || i)::integer ON EMPTY ERROR ON ERROR) > i) + "test_jsonb_constraint4" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb) + "test_jsonb_constraint5" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C")) + "test_jsonb_constraint6" CHECK (JSON_EXISTS(js::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2) + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + check_clause +-------------------------------------------------------------------------------------------------------------------------- + ((JSON_EXISTS((js)::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2)) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C"))) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb)) + ((JSON_VALUE((js)::jsonb, '$."a"' RETURNING integer DEFAULT (('12'::text || i))::integer ON EMPTY ERROR ON ERROR) > i)) + ((js IS JSON)) + (JSON_EXISTS((js)::jsonb, '$."a"' PASSING (i + 5) AS int, (i)::text AS txt, ARRAY[1, 2, 3] AS arr)) +(6 rows) + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +WHERE constraint_name LIKE 'test_jsonb_constraint%'; +ERROR: syntax error at or near "WHERE" +LINE 1: WHERE constraint_name LIKE 'test_jsonb_constraint%'; + ^ +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint1" +DETAIL: Failing row contains (, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains (1, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('[]'); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ([], null, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ({"b": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint3" +DETAIL: Failing row contains ({"a": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint5" +DETAIL: Failing row contains ({"a": 7}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint4" +DETAIL: Failing row contains ({"a": 10}, 1, [1, 2]). +DROP TABLE test_jsonb_constraints; +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 4df9d8503b..9b0ecf049d 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -103,7 +103,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo # ---------- # Another group of parallel tests (JSON related) # ---------- -test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson +test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson json_sqljson jsonb_sqljson # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql new file mode 100644 index 0000000000..4f30fa46b9 --- /dev/null +++ b/src/test/regress/sql/json_sqljson.sql @@ -0,0 +1,11 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + +-- JSON_QUERY + +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql new file mode 100644 index 0000000000..b8a6148b7b --- /dev/null +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -0,0 +1,327 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL::jsonb, '$'); + +SELECT JSON_EXISTS(jsonb '[]', '$'); +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + +SELECT JSON_EXISTS(jsonb '1', '$'); +SELECT JSON_EXISTS(jsonb 'null', '$'); +SELECT JSON_EXISTS(jsonb '[]', '$'); + +SELECT JSON_EXISTS(jsonb '1', '$.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_EXISTS(jsonb 'null', '$.a'); +SELECT JSON_EXISTS(jsonb '[]', '$.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); +SELECT JSON_EXISTS(jsonb '{}', '$.a'); +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSON); -- RETURNING FORMAT not allowed + + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL::jsonb, '$'); + +SELECT JSON_VALUE(jsonb 'null', '$'); +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + +SELECT JSON_VALUE(jsonb 'true', '$'); +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + +SELECT JSON_VALUE(jsonb '123', '$'); +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1.23', '$'); +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '"aaa"', '$'); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); + +SELECT JSON_VALUE(jsonb '[]', '$'); +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '{}', '$'); +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1', '$.a'); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed + +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- JSON_QUERY + +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); + +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); + +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); + +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- Test constraints + +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); + +\d test_jsonb_constraints + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; +WHERE constraint_name LIKE 'test_jsonb_constraint%'; + +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +INSERT INTO test_jsonb_constraints VALUES ('[]'); +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); + +DROP TABLE test_jsonb_constraints; + +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8efc61cb55..211a223193 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1254,14 +1254,24 @@ JsonArrayAgg JsonArrayConstructor JsonArrayQueryConstructor JsonBaseObjectInfo +JsonBehavior +JsonBehaviorType +JsonCoercion +JsonCoercionState +JsonCommon JsonConstructorExpr JsonConstructorExprState JsonConstructorType JsonEncoding +JsonExpr +JsonExprOp +JsonExprState JsonFormat JsonFormatType JsonHashEntry JsonIsPredicate +JsonItemCoercions +JsonItemCoercionsState JsonIterateStringValuesAction JsonKeyValue JsonLexContext @@ -1294,6 +1304,10 @@ JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback JsonPathString +JsonPathVarCallback +JsonPathVariable +JsonPathVariableEvalContext +JsonQuotes JsonReturning JsonSemAction JsonSerializeExpr -- 2.35.3
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-20 15:48  Amit Langote <amitlangote09@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-07-20 15:48 UTC (permalink / raw)
  To: Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers; jian he <jian.universality@gmail.com>

On Thu, Jul 20, 2023 at 17:19 Amit Langote <amitlangote09@gmail.com> wrote:

> On Wed, Jul 19, 2023 at 5:17 PM Amit Langote <amitlangote09@gmail.com>
> wrote:
> > On Wed, Jul 19, 2023 at 12:53 AM Alvaro Herrera <alvherre@alvh.no-ip.org>
> wrote:
> > > On 2023-Jul-18, Amit Langote wrote:
> > >
> > > > Attached updated patches.  In 0002, I removed the mention of the
> > > > RETURNING clause in the JSON(), JSON_SCALAR() documentation, which I
> > > > had forgotten to do in the last version which removed its support in
> > > > code.
> > >
> > > > I think 0001 looks ready to go.  Alvaro?
> > >
> > > It looks reasonable to me.
> >
> > Thanks for taking another look.
> >
> > I will push this tomorrow.
>
> Pushed.
>
> > > > Also, I've been wondering if it isn't too late to apply the following
> > > > to v16 too, so as to make the code look similar in both branches:
> > >
> > > Hmm.
> > >
> > > > 785480c953 Pass constructName to transformJsonValueExpr()
> > >
> > > I think 785480c953 can easily be considered a bugfix on 7081ac46ace8,
> so
> > > I agree it's better to apply it to 16.
> >
> > OK.
>
> Pushed to 16.
>
> > > > b6e1157e7d Don't include CaseTestExpr in JsonValueExpr.formatted_expr
> > >
> > > I feel a bit uneasy about this one.  It seems to assume that
> > > formatted_expr is always set, but at the same time it's not obvious
> that
> > > it is.  (Maybe this aspect just needs some more commentary).
> >
> > Hmm, I agree that the comments about formatted_expr could be improved
> > further, for which I propose the attached.  Actually, staring some
> > more at this, I'm inclined to change makeJsonValueExpr() to allow
> > callers to pass it the finished 'formatted_expr' rather than set it by
> > themselves.
> >
> > >  I agree
> > > that it would be better to make both branches identical, because if
> > > there's a problem, we are better equipped to get a fix done to both.
> > >
> > > As for the removal of makeCaseTestExpr(), I agree -- of the six callers
> > > of makeNode(CastTestExpr), only two of them would be able to use the
> new
> > > function, so it doesn't look of general enough usefulness.
> >
> > OK, so you agree with back-patching this one too, though perhaps only
> > after applying something like the aforementioned patch.
>
> I looked at this some more and concluded that it's fine to think that
> all JsonValueExpr nodes leaving the parser have their formatted_expr
> set.  I've updated the commentary some more in the patch attached as
> 0001.
>
> Rebased SQL/JSON patches also attached.  I've fixed the JSON_TABLE
> syntax synopsis in the documentation as mentioned in my other email.


I’m thinking of pushing 0001 and 0002 tomorrow barring objections.

> --
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-20 16:02  Alvaro Herrera <alvherre@alvh.no-ip.org>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Alvaro Herrera @ 2023-07-20 16:02 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers; jian he <jian.universality@gmail.com>

On 2023-Jul-21, Amit Langote wrote:

> I’m thinking of pushing 0001 and 0002 tomorrow barring objections.

0001 looks reasonable to me.  I think you asked whether to squash that
one with the other bugfix commit for the same code that you already
pushed to master; I think there's no point in committing as separate
patches, because the first one won't show up in the git_changelog output
as a single entity with the one in 16, so it'll just be additional
noise.

I've looked at 0002 at various points in time and I think it looks
generally reasonable.  I think your removal of a couple of newlines
(where originally two appear in sequence) is unwarranted; that the name
to_json[b]_worker is ugly for exported functions (maybe "datum_to_json"
would be better, or you may have better ideas); and that the omission of
the stock comment in the new stanzas in FigureColnameInternal() is
strange.  But I don't have anything serious.  Do add some ecpg tests ...

Also, remember to pgindent and bump catversion, if you haven't already.

-- 
Álvaro Herrera               48°01'N 7°57'E  —  https://www.EnterpriseDB.com/
"No hay hombre que no aspire a la plenitud, es decir,
la suma de experiencias de que un hombre es capaz"





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-21 10:33  Amit Langote <amitlangote09@gmail.com>
  parent: Alvaro Herrera <alvherre@alvh.no-ip.org>
  0 siblings, 3 replies; 300+ messages in thread

From: Amit Langote @ 2023-07-21 10:33 UTC (permalink / raw)
  To: Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers; jian he <jian.universality@gmail.com>

Hi Alvaro,

Thanks for taking a look.

On Fri, Jul 21, 2023 at 1:02 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
> On 2023-Jul-21, Amit Langote wrote:
>
> > I’m thinking of pushing 0001 and 0002 tomorrow barring objections.
>
> 0001 looks reasonable to me.  I think you asked whether to squash that
> one with the other bugfix commit for the same code that you already
> pushed to master; I think there's no point in committing as separate
> patches, because the first one won't show up in the git_changelog output
> as a single entity with the one in 16, so it'll just be additional
> noise.

OK, pushed 0001 to HEAD and b6e1157e7d + 0001 to 16.

> I've looked at 0002 at various points in time and I think it looks
> generally reasonable.  I think your removal of a couple of newlines
> (where originally two appear in sequence) is unwarranted; that the name
> to_json[b]_worker is ugly for exported functions (maybe "datum_to_json"
> would be better, or you may have better ideas);

Went with datum_to_json[b].  Created a separate refactoring patch for
this, attached as 0001.

Created another refactoring patch for the hunks related to renaming of
a nonterminal in gram.y, attached as 0002.

> and that the omission of
> the stock comment in the new stanzas in FigureColnameInternal() is
> strange.

Yes, fixed.

>  But I don't have anything serious.  Do add some ecpg tests ...

Added.

> Also, remember to pgindent and bump catversion, if you haven't already.

Will do.  Wasn't sure myself whether the catversion should be bumped,
but I suppose it must be because ruleutils.c has changed.

Attaching latest patches.  Will push 0001, 0002, and 0003 on Monday to
avoid worrying about the buildfarm on a Friday evening.

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

Attachments: download all (6 files, .tar.gz)

v10-0002-Rename-a-nonterminal-used-in-SQL-JSON-grammar.patch (application/octet-stream, 3.3K) download From 4b40008b5055c02241b8e5b77ad26bc588035a13 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Thu, 20 Jul 2023 22:21:43 +0900 Subject: [PATCH v10 2/6] Rename a nonterminal used in SQL/JSON grammar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This renames json_output_clause_opt to json_returning_clause_opt, because the new name makes more sense given that the governing keyword is RETURNING. Per suggestion from Álvaro Herrera. Discussion: https://postgr.es/m/20230707122820.wy5zlmhn4tdzbojl%40alvherre.pgsql --- src/backend/parser/gram.y | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 60080e877e..e7134add11 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -647,7 +647,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> json_format_clause_opt json_value_expr - json_output_clause_opt + json_returning_clause_opt json_name_and_value json_aggregate_func %type <list> json_name_and_value_list @@ -15570,7 +15570,7 @@ func_expr_common_subexpr: | JSON_OBJECT '(' json_name_and_value_list json_object_constructor_null_clause_opt json_key_uniqueness_constraint_opt - json_output_clause_opt ')' + json_returning_clause_opt ')' { JsonObjectConstructor *n = makeNode(JsonObjectConstructor); @@ -15581,7 +15581,7 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } - | JSON_OBJECT '(' json_output_clause_opt ')' + | JSON_OBJECT '(' json_returning_clause_opt ')' { JsonObjectConstructor *n = makeNode(JsonObjectConstructor); @@ -15595,7 +15595,7 @@ func_expr_common_subexpr: | JSON_ARRAY '(' json_value_expr_list json_array_constructor_null_clause_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayConstructor *n = makeNode(JsonArrayConstructor); @@ -15610,7 +15610,7 @@ func_expr_common_subexpr: select_no_parens json_format_clause_opt /* json_array_constructor_null_clause_opt */ - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayQueryConstructor *n = makeNode(JsonArrayQueryConstructor); @@ -15623,7 +15623,7 @@ func_expr_common_subexpr: $$ = (Node *) n; } | JSON_ARRAY '(' - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayConstructor *n = makeNode(JsonArrayConstructor); @@ -16384,7 +16384,7 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; -json_output_clause_opt: +json_returning_clause_opt: RETURNING Typename json_format_clause_opt { JsonOutput *n = makeNode(JsonOutput); @@ -16457,7 +16457,7 @@ json_aggregate_func: json_name_and_value json_object_constructor_null_clause_opt json_key_uniqueness_constraint_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonObjectAgg *n = makeNode(JsonObjectAgg); @@ -16475,7 +16475,7 @@ json_aggregate_func: json_value_expr json_array_aggregate_order_by_clause_opt json_array_constructor_null_clause_opt - json_output_clause_opt + json_returning_clause_opt ')' { JsonArrayAgg *n = makeNode(JsonArrayAgg); -- 2.35.3
v10-0006-Claim-SQL-standard-compliance-for-SQL-JSON-featu.patch (application/octet-stream, 2.3K) download From c0670fa46779ebf03526a5454b74c482aae69d9b Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Tue, 18 Jul 2023 17:58:44 +0900 Subject: [PATCH v10 6/6] Claim SQL standard compliance for SQL/JSON features Discussion: https://postgr.es/m/d03d809c-d0fb-fd6a-1476-d6dc18ec940e@dunslane.net --- src/backend/catalog/sql_features.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt index b33065d7bf..b379c71df9 100644 --- a/src/backend/catalog/sql_features.txt +++ b/src/backend/catalog/sql_features.txt @@ -548,15 +548,15 @@ T811 Basic SQL/JSON constructor functions YES T812 SQL/JSON: JSON_OBJECTAGG YES T813 SQL/JSON: JSON_ARRAYAGG with ORDER BY YES T814 Colon in JSON_OBJECT or JSON_OBJECTAGG YES -T821 Basic SQL/JSON query operators NO +T821 Basic SQL/JSON query operators YES T822 SQL/JSON: IS JSON WITH UNIQUE KEYS predicate YES -T823 SQL/JSON: PASSING clause NO -T824 JSON_TABLE: specific PLAN clause NO -T825 SQL/JSON: ON EMPTY and ON ERROR clauses NO -T826 General value expression in ON ERROR or ON EMPTY clauses NO -T827 JSON_TABLE: sibling NESTED COLUMNS clauses NO -T828 JSON_QUERY NO -T829 JSON_QUERY: array wrapper options NO +T823 SQL/JSON: PASSING clause YES +T824 JSON_TABLE: specific PLAN clause YES +T825 SQL/JSON: ON EMPTY and ON ERROR clauses YES +T826 General value expression in ON ERROR or ON EMPTY clauses YES +T827 JSON_TABLE: sibling NESTED COLUMNS clauses YES +T828 JSON_QUERY YES +T829 JSON_QUERY: array wrapper options YES T830 Enforcing unique keys in SQL/JSON constructor functions YES T831 SQL/JSON path language: strict mode YES T832 SQL/JSON path language: item method YES @@ -565,7 +565,7 @@ T834 SQL/JSON path language: wildcard member accessor YES T835 SQL/JSON path language: filter expressions YES T836 SQL/JSON path language: starts with predicate YES T837 SQL/JSON path language: regex_like predicate YES -T838 JSON_TABLE: PLAN DEFAULT clause NO +T838 JSON_TABLE: PLAN DEFAULT clause YES T839 Formatted cast of datetimes to/from character strings NO T840 Hex integer literals in SQL/JSON path language YES T851 SQL/JSON: optional keywords for default syntax YES -- 2.35.3
v10-0003-Add-more-SQL-JSON-constructor-functions.patch (application/octet-stream, 71.7K) download From 64643745d9733f32d7b1afac955fc682f82f2906 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Thu, 20 Jul 2023 22:21:43 +0900 Subject: [PATCH v10 3/6] Add more SQL/JSON constructor functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This Patch introduces three SQL standard JSON functions: JSON() JSON_SCALAR() JSON_SERIALIZE() JSON() produces json values from text, bytea, json or jsonb values, and has facilitites for handling duplicate keys. JSON_SCALAR() produces a json value from any scalar sql value, including json and jsonb. JSON_SERIALIZE() produces text or bytea from input which containis or represents json or jsonb; For the most part these functions don't add any significant new capabilities, but they will be of use to users wanting standard compliant JSON handling. Author: Nikita Glukhov <n.gluhov@postgrespro.ru> Author: Teodor Sigaev <teodor@sigaev.ru> Author: Oleg Bartunov <obartunov@gmail.com> Author: Alexander Korotkov <aekorotkov@gmail.com> Author: Andrew Dunstan <andrew@dunslane.net> Author: Amit Langote <amitlangote09@gmail.com> Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera, Peter Eisentraut Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- doc/src/sgml/func.sgml | 64 ++++ src/backend/executor/execExpr.c | 30 ++ src/backend/executor/execExprInterp.c | 43 ++- src/backend/nodes/nodeFuncs.c | 30 ++ src/backend/parser/gram.y | 49 ++- src/backend/parser/parse_expr.c | 221 +++++++++++- src/backend/parser/parse_target.c | 12 + src/backend/utils/adt/format_type.c | 4 + src/backend/utils/adt/jsonb.c | 15 +- src/backend/utils/adt/ruleutils.c | 16 +- src/include/nodes/parsenodes.h | 48 +++ src/include/nodes/primnodes.h | 5 +- src/include/parser/kwlist.h | 4 +- src/include/utils/jsonb.h | 1 - src/include/utils/jsonfuncs.h | 2 +- .../ecpg/test/expected/sql-sqljson.c | 204 ++++++++++- .../ecpg/test/expected/sql-sqljson.stderr | 201 ++++++++++- .../ecpg/test/expected/sql-sqljson.stdout | 16 + src/interfaces/ecpg/test/sql/sqljson.pgc | 60 ++++ src/test/regress/expected/sqljson.out | 332 ++++++++++++++++++ src/test/regress/sql/sqljson.sql | 85 +++++ src/tools/pgindent/typedefs.list | 4 + 22 files changed, 1405 insertions(+), 41 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index b94827674c..feb39e5352 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16001,6 +16001,70 @@ table2-mapping <returnvalue>{"a": "1", "b": "2"}</returnvalue> </para></entry> </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json constructor</primary></indexterm> + <function>json</function> ( + <replaceable>expression</replaceable> + <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional></optional> + <optional> { <literal>WITH</literal> | <literal>WITHOUT</literal> } <literal>UNIQUE</literal> <optional> <literal>KEYS</literal> </optional></optional> + </para> + <para> + Converts a given expression specified as <type>text</type> or + <type>bytea</type> string (in UTF8 encoding) into a JSON + value. If <replaceable>expression</replaceable> is NULL, an + <acronym>SQL</acronym> null value is returned. + If <literal>WITH UNIQUE</literal> is specified, the + <replaceable>expression</replaceable> must not contain any duplicate + object keys. + </para> + <para> + <literal>json('{"a":123, "b":[true,"foo"], "a":"bar"}')</literal> + <returnvalue>{"a":123, "b":[true,"foo"], "a":"bar"}</returnvalue> + </para> + </entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <indexterm><primary>json_scalar</primary></indexterm> + <function>json_scalar</function> (<replaceable>expression</replaceable>) + </para> + <para> + Converts a given SQL scalar value into a JSON scalar value. + If the input is NULL, an SQL NULL is returned. If the input is number + or a boolean value, a corresponding JSON number or boolean value is + returned. For any other value, a JSON string is returned. + </para> + <para> + <literal>json_scalar(123.45)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_scalar(CURRENT_TIMESTAMP)</literal> + <returnvalue>"2022-05-10T10:51:04.62128-04:00"</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"> + <para role="func_signature"> + <function>json_serialize</function> ( + <replaceable>expression</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional>) + </para> + <para> + Converts an SQL/JSON expression into a character or binary string. The + <replaceable>expression</replaceable> can be of any JSON type, any + character string type, or <type>bytea</type> in UTF8 encoding. + The returned type can be any character string type or + <type>bytea</type>. The default is <type>text</type>. + </para> + <para> + <literal>json_serialize('{ "a" : 1 } ' RETURNING bytea)</literal> + <returnvalue>\x7b20226122203a2031207d20</returnvalue> + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index bf3a08c5f0..2c62b0c9c8 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -48,6 +48,7 @@ #include "utils/array.h" #include "utils/builtins.h" #include "utils/datum.h" +#include "utils/jsonfuncs.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -2311,6 +2312,12 @@ ExecInitExprRec(Expr *node, ExprState *state, { ExecInitExprRec(ctor->func, state, resv, resnull); } + else if ((ctor->type == JSCTOR_JSON_PARSE && !ctor->unique) || + ctor->type == JSCTOR_JSON_SERIALIZE) + { + /* Use the value of the first argument as result */ + ExecInitExprRec(linitial(args), state, resv, resnull); + } else { JsonConstructorExprState *jcstate; @@ -2349,6 +2356,29 @@ ExecInitExprRec(Expr *node, ExprState *state, argno++; } + /* prepare type cache for datum_to_json[b]() */ + if (ctor->type == JSCTOR_JSON_SCALAR) + { + bool is_jsonb = + ctor->returning->format->format_type == JS_FORMAT_JSONB; + + jcstate->arg_type_cache = + palloc(sizeof(*jcstate->arg_type_cache) * nargs); + + for (int i = 0; i < nargs; i++) + { + JsonTypeCategory category; + Oid outfuncid; + Oid typid = jcstate->arg_types[i]; + + json_categorize_type(typid, is_jsonb, + &category, &outfuncid); + + jcstate->arg_type_cache[i].outfuncid = outfuncid; + jcstate->arg_type_cache[i].category = (int) category; + } + } + ExprEvalPushStep(state, &scratch); } diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 851946a927..47665a9cdf 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -3992,7 +3992,7 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_values, jcstate->arg_nulls, jcstate->arg_types, - jcstate->constructor->absent_on_null); + ctor->absent_on_null); else if (ctor->type == JSCTOR_JSON_OBJECT) res = (is_jsonb ? jsonb_build_object_worker : @@ -4002,6 +4002,47 @@ ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, jcstate->arg_types, jcstate->constructor->absent_on_null, jcstate->constructor->unique); + else if (ctor->type == JSCTOR_JSON_SCALAR) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + Oid outfuncid = jcstate->arg_type_cache[0].outfuncid; + JsonTypeCategory category = (JsonTypeCategory) + jcstate->arg_type_cache[0].category; + + if (is_jsonb) + res = datum_to_jsonb(value, category, outfuncid); + else + res = datum_to_json(value, category, outfuncid); + } + } + else if (ctor->type == JSCTOR_JSON_PARSE) + { + if (jcstate->arg_nulls[0]) + { + res = (Datum) 0; + isnull = true; + } + else + { + Datum value = jcstate->arg_values[0]; + text *js = DatumGetTextP(value); + + if (is_jsonb) + res = jsonb_from_text(js, true); + else + { + (void) json_validate(js, true, true); + res = value; + } + } + } else elog(ERROR, "invalid JsonConstructorExpr type %d", ctor->type); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 503d76aae0..c03f4f23e2 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -3899,6 +3899,36 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonParseExpr: + { + JsonParseExpr *jpe = (JsonParseExpr *) node; + + if (WALK(jpe->expr)) + return true; + if (WALK(jpe->output)) + return true; + } + break; + case T_JsonScalarExpr: + { + JsonScalarExpr *jse = (JsonScalarExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; + case T_JsonSerializeExpr: + { + JsonSerializeExpr *jse = (JsonSerializeExpr *) node; + + if (WALK(jse->expr)) + return true; + if (WALK(jse->output)) + return true; + } + break; case T_JsonConstructorExpr: { JsonConstructorExpr *ctor = (JsonConstructorExpr *) node; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index e7134add11..856d5dee0e 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -566,7 +566,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <list> copy_options %type <typnam> Typename SimpleTypename ConstTypename - GenericType Numeric opt_float + GenericType Numeric opt_float JsonType Character ConstCharacter CharacterWithLength CharacterWithoutLength ConstDatetime ConstInterval @@ -723,6 +723,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG + JSON_SCALAR JSON_SERIALIZE KEY KEYS @@ -13990,6 +13991,7 @@ SimpleTypename: $$->typmods = list_make2(makeIntConst(INTERVAL_FULL_RANGE, -1), makeIntConst($3, @3)); } + | JsonType { $$ = $1; } ; /* We have a separate ConstTypename to allow defaulting fixed-length @@ -14008,6 +14010,7 @@ ConstTypename: | ConstBit { $$ = $1; } | ConstCharacter { $$ = $1; } | ConstDatetime { $$ = $1; } + | JsonType { $$ = $1; } ; /* @@ -14376,6 +14379,13 @@ interval_second: } ; +JsonType: + JSON + { + $$ = SystemTypeName("json"); + $$->location = @1; + } + ; /***************************************************************************** * @@ -15634,7 +15644,36 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } - ; + | JSON '(' json_value_expr json_key_uniqueness_constraint_opt ')' + { + JsonParseExpr *n = makeNode(JsonParseExpr); + + n->expr = (JsonValueExpr *) $3; + n->unique_keys = $4; + n->output = NULL; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SCALAR '(' a_expr ')' + { + JsonScalarExpr *n = makeNode(JsonScalarExpr); + + n->expr = (Expr *) $3; + n->output = NULL; + n->location = @1; + $$ = (Node *) n; + } + | JSON_SERIALIZE '(' json_value_expr json_returning_clause_opt ')' + { + JsonSerializeExpr *n = makeNode(JsonSerializeExpr); + + n->expr = (JsonValueExpr *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + ; + /* * SQL/XML support @@ -17075,7 +17114,6 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION - | JSON | KEY | KEYS | LABEL @@ -17290,10 +17328,13 @@ col_name_keyword: | INT_P | INTEGER | INTERVAL + | JSON | JSON_ARRAY | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | LEAST | NATIONAL | NCHAR @@ -17654,6 +17695,8 @@ bare_label_keyword: | JSON_ARRAYAGG | JSON_OBJECT | JSON_OBJECTAGG + | JSON_SCALAR + | JSON_SERIALIZE | KEY | KEYS | LABEL diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index c08c06373a..12f8777bfd 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -86,6 +86,10 @@ static Node *transformJsonArrayQueryConstructor(ParseState *pstate, static Node *transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg); static Node *transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg); static Node *transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred); +static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); +static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); +static Node *transformJsonSerializeExpr(ParseState *pstate, + JsonSerializeExpr *expr); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -337,6 +341,18 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonIsPredicate(pstate, (JsonIsPredicate *) expr); break; + case T_JsonParseExpr: + result = transformJsonParseExpr(pstate, (JsonParseExpr *) expr); + break; + + case T_JsonScalarExpr: + result = transformJsonScalarExpr(pstate, (JsonScalarExpr *) expr); + break; + + case T_JsonSerializeExpr: + result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3212,7 +3228,8 @@ makeJsonByteaToTextConversion(Node *expr, JsonFormat *format, int location) */ static Node * transformJsonValueExpr(ParseState *pstate, const char *constructName, - JsonValueExpr *ve, JsonFormatType default_format) + JsonValueExpr *ve, JsonFormatType default_format, + Oid targettype) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3254,12 +3271,14 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, else format = default_format; - if (format != JS_FORMAT_DEFAULT) + if (format != JS_FORMAT_DEFAULT || + (OidIsValid(targettype) && exprtype != targettype)) { - Oid targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; Node *coerced; + bool only_allow_cast = OidIsValid(targettype); - if (exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) + if (!only_allow_cast && + exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), errmsg(ve->format->format_type == JS_FORMAT_DEFAULT ? @@ -3275,6 +3294,9 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, exprtype = TEXTOID; } + if (!OidIsValid(targettype)) + targettype = format == JS_FORMAT_JSONB ? JSONBOID : JSONOID; + /* Try to coerce to the target type. */ coerced = coerce_to_target_type(pstate, expr, exprtype, targettype, -1, @@ -3285,11 +3307,24 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, if (!coerced) { /* If coercion failed, use to_json()/to_jsonb() functions. */ - Oid fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; - FuncExpr *fexpr = makeFuncExpr(fnoid, targettype, - list_make1(expr), - InvalidOid, InvalidOid, - COERCE_EXPLICIT_CALL); + FuncExpr *fexpr; + Oid fnoid; + + /* + * Though only allow a cast when the target type is specified by + * the caller. + */ + if (only_allow_cast) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(exprtype), + format_type_be(targettype)), + parser_errposition(pstate, location))); + + fnoid = targettype == JSONOID ? F_TO_JSON : F_TO_JSONB; + fexpr = makeFuncExpr(fnoid, targettype, list_make1(expr), + InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL); fexpr->location = location; @@ -3590,7 +3625,8 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) Node *key = transformExprRecurse(pstate, (Node *) kv->key); Node *val = transformJsonValueExpr(pstate, "JSON_OBJECT()", kv->value, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, key); args = lappend(args, val); @@ -3776,7 +3812,8 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) key = transformExprRecurse(pstate, (Node *) agg->arg->key); val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", agg->arg->value, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, + InvalidOid); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3834,7 +3871,7 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", agg->arg, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, InvalidOid); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3882,7 +3919,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, "JSON_ARRAY()", jsval, - JS_FORMAT_DEFAULT); + JS_FORMAT_DEFAULT, + InvalidOid); args = lappend(args, val); } @@ -3963,3 +4001,160 @@ transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred) return makeJsonIsPredicate(expr, NULL, pred->item_type, pred->unique_keys, pred->location); } + +/* + * Transform the RETURNING clause of a JSON_*() expression if there is one and + * create one if not. + */ +static JsonReturning * +transformJsonReturning(ParseState *pstate, JsonOutput *output, const char *fname) +{ + JsonReturning *returning; + + if (output) + { + returning = transformJsonOutput(pstate, output, false); + + Assert(OidIsValid(returning->typid)); + + if (returning->typid != JSONOID && returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), fname), + parser_errposition(pstate, output->typeName->location))); + } + else + { + /* Output type is JSON by default. */ + Oid targettype = JSONOID; + JsonFormatType format = JS_FORMAT_JSON; + + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(format, JS_ENC_DEFAULT, -1); + returning->typid = targettype; + returning->typmod = -1; + } + + return returning; +} + +/* + * Transform a JSON() expression. + * + * JSON() is transformed into a JsonConstructorExpr of type JSCTOR_JSON_PARSE, + * which validates the input expression value as JSON. + */ +static Node * +transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) +{ + JsonOutput *output = jsexpr->output; + JsonReturning *returning; + Node *arg; + + returning = transformJsonReturning(pstate, output, "JSON()"); + + if (jsexpr->unique_keys) + { + /* + * Coerce string argument to text and then to json[b] in the executor + * node with key uniqueness check. + */ + JsonValueExpr *jve = jsexpr->expr; + Oid arg_type; + + arg = transformJsonParseArg(pstate, (Node *) jve->raw_expr, jve->format, + &arg_type); + + if (arg_type != TEXTOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use non-string types with WITH UNIQUE KEYS clause"), + parser_errposition(pstate, jsexpr->location))); + } + else + { + /* + * Coerce argument to target type using CAST for compatibility with PG + * function-like CASTs. + */ + arg = transformJsonValueExpr(pstate, "JSON()", jsexpr->expr, + JS_FORMAT_JSON, returning->typid); + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, + returning, jsexpr->unique_keys, false, + jsexpr->location); +} + +/* + * Transform a JSON_SCALAR() expression. + * + * JSON_SCALAR() is transformed into a JsonConstructorExpr of type + * JSCTOR_JSON_SCALAR, which converts the input SQL scalar value into + * a json[b] value. + */ +static Node * +transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *jsexpr) +{ + Node *arg = transformExprRecurse(pstate, (Node *) jsexpr->expr); + JsonOutput *output = jsexpr->output; + JsonReturning *returning; + + returning = transformJsonReturning(pstate, output, "JSON_SCALAR()"); + + if (exprType(arg) == UNKNOWNOID) + arg = coerce_to_specific_type(pstate, arg, TEXTOID, "JSON_SCALAR"); + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SCALAR, list_make1(arg), NULL, + returning, false, false, jsexpr->location); +} + +/* + * Transform a JSON_SERIALIZE() expression. + * + * JSON_SERIALIZE() is transformed into a JsonConstructorExpr of type + * JSCTOR_JSON_SERIALIZE which converts the input JSON value into a character + * or bytea string. + */ +static Node * +transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr *expr) +{ + JsonReturning *returning; + Node *arg = transformJsonValueExpr(pstate, "JSON_SERIALIZE()", + expr->expr, + JS_FORMAT_JSON, + InvalidOid); + + if (expr->output) + { + returning = transformJsonOutput(pstate, expr->output, true); + + if (returning->typid != BYTEAOID) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(returning->typid, &typcategory, + &typispreferred); + if (typcategory != TYPCATEGORY_STRING) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use RETURNING type %s in %s", + format_type_be(returning->typid), + "JSON_SERIALIZE()"), + errhint("Try returning a string type or bytea."))); + } + } + else + { + /* RETURNING TEXT FORMAT JSON is by default */ + returning = makeNode(JsonReturning); + returning->format = makeJsonFormat(JS_FORMAT_JSON, JS_ENC_DEFAULT, -1); + returning->typid = TEXTOID; + returning->typmod = -1; + } + + return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), + NULL, returning, false, false, expr->location); +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4cca97ff9c..57247de363 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1953,6 +1953,18 @@ FigureColnameInternal(Node *node, char **name) /* make XMLSERIALIZE act like a regular function */ *name = "xmlserialize"; return 2; + case T_JsonParseExpr: + /* make JSON act like a regular function */ + *name = "json"; + return 2; + case T_JsonScalarExpr: + /* make JSON_SCALAR act like a regular function */ + *name = "json_scalar"; + return 2; + case T_JsonSerializeExpr: + /* make JSON_SERIALIZE act like a regular function */ + *name = "json_serialize"; + return 2; case T_JsonObjectConstructor: /* make JSON_OBJECT act like a regular function */ *name = "json_object"; diff --git a/src/backend/utils/adt/format_type.c b/src/backend/utils/adt/format_type.c index 12402a0637..36c45a3978 100644 --- a/src/backend/utils/adt/format_type.c +++ b/src/backend/utils/adt/format_type.c @@ -294,6 +294,10 @@ format_type_extended(Oid type_oid, int32 typemod, bits16 flags) else buf = pstrdup("character varying"); break; + + case JSONOID: + buf = pstrdup("json"); + break; } if (buf == NULL) diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index 5ea582a888..9781852b0c 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -33,6 +33,7 @@ typedef struct JsonbInState { JsonbParseState *parseState; JsonbValue *res; + bool unique_keys; Node *escontext; } JsonbInState; @@ -45,7 +46,8 @@ typedef struct JsonbAggState Oid val_output_func; } JsonbAggState; -static inline Datum jsonb_from_cstring(char *json, int len, Node *escontext); +static inline Datum jsonb_from_cstring(char *json, int len, bool unique_keys, + Node *escontext); static bool checkStringLen(size_t len, Node *escontext); static JsonParseErrorType jsonb_in_object_start(void *pstate); static JsonParseErrorType jsonb_in_object_end(void *pstate); @@ -76,7 +78,7 @@ jsonb_in(PG_FUNCTION_ARGS) { char *json = PG_GETARG_CSTRING(0); - return jsonb_from_cstring(json, strlen(json), fcinfo->context); + return jsonb_from_cstring(json, strlen(json), false, fcinfo->context); } /* @@ -100,7 +102,7 @@ jsonb_recv(PG_FUNCTION_ARGS) else elog(ERROR, "unsupported jsonb version number %d", version); - return jsonb_from_cstring(str, nbytes, NULL); + return jsonb_from_cstring(str, nbytes, false, NULL); } /* @@ -147,10 +149,11 @@ jsonb_send(PG_FUNCTION_ARGS) * Turns json text string into a jsonb Datum. */ Datum -jsonb_from_text(text *js) +jsonb_from_text(text *js, bool unique_keys) { return jsonb_from_cstring(VARDATA_ANY(js), VARSIZE_ANY_EXHDR(js), + unique_keys, NULL); } @@ -247,7 +250,7 @@ jsonb_typeof(PG_FUNCTION_ARGS) * instead of being thrown. */ static inline Datum -jsonb_from_cstring(char *json, int len, Node *escontext) +jsonb_from_cstring(char *json, int len, bool unique_keys, Node *escontext) { JsonLexContext *lex; JsonbInState state; @@ -257,6 +260,7 @@ jsonb_from_cstring(char *json, int len, Node *escontext) memset(&sem, 0, sizeof(sem)); lex = makeJsonLexContextCstringLen(json, len, GetDatabaseEncoding(), true); + state.unique_keys = unique_keys; state.escontext = escontext; sem.semstate = (void *) &state; @@ -293,6 +297,7 @@ jsonb_in_object_start(void *pstate) JsonbInState *_state = (JsonbInState *) pstate; _state->res = pushJsonbValue(&_state->parseState, WJB_BEGIN_OBJECT, NULL); + _state->parseState->unique_keys = _state->unique_keys; return JSON_SUCCESS; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index fcb2f45f62..03f2835c3f 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -10832,6 +10832,15 @@ get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, case JSCTOR_JSON_ARRAY: funcname = "JSON_ARRAY"; break; + case JSCTOR_JSON_PARSE: + funcname = "JSON"; + break; + case JSCTOR_JSON_SCALAR: + funcname = "JSON_SCALAR"; + break; + case JSCTOR_JSON_SERIALIZE: + funcname = "JSON_SERIALIZE"; + break; default: elog(ERROR, "invalid JsonConstructorType %d", ctor->type); } @@ -10879,7 +10888,12 @@ get_json_constructor_options(JsonConstructorExpr *ctor, StringInfo buf) if (ctor->unique) appendStringInfoString(buf, " WITH UNIQUE KEYS"); - get_json_returning(ctor->returning, buf, true); + /* + * Append RETURNING clause if needed; JSON() and JSON_SCALAR() don't + * support one. + */ + if (ctor->type != JSCTOR_JSON_PARSE && ctor->type != JSCTOR_JSON_SCALAR) + get_json_returning(ctor->returning, buf, true); } /* diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index efb5c3e098..d926713bd9 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1716,6 +1716,17 @@ typedef struct TriggerTransition /* Nodes for SQL/JSON support */ +/* + * JsonQuotes - + * representation of [KEEP|OMIT] QUOTES clause for JSON_QUERY() + */ +typedef enum JsonQuotes +{ + JS_QUOTES_UNSPEC, /* unspecified */ + JS_QUOTES_KEEP, /* KEEP QUOTES */ + JS_QUOTES_OMIT /* OMIT QUOTES */ +} JsonQuotes; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1739,6 +1750,43 @@ typedef struct JsonKeyValue JsonValueExpr *value; /* JSON value expression */ } JsonKeyValue; +/* + * JsonParseExpr - + * untransformed representation of JSON() + */ +typedef struct JsonParseExpr +{ + NodeTag type; + JsonValueExpr *expr; /* string expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + bool unique_keys; /* WITH UNIQUE KEYS? */ + int location; /* token location, or -1 if unknown */ +} JsonParseExpr; + +/* + * JsonScalarExpr - + * untransformed representation of JSON_SCALAR() + */ +typedef struct JsonScalarExpr +{ + NodeTag type; + Expr *expr; /* scalar expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonScalarExpr; + +/* + * JsonSerializeExpr - + * untransformed representation of JSON_SERIALIZE() function + */ +typedef struct JsonSerializeExpr +{ + NodeTag type; + JsonValueExpr *expr; /* json value expression */ + JsonOutput *output; /* RETURNING clause, if specified */ + int location; /* token location, or -1 if unknown */ +} JsonSerializeExpr; + /* * JsonObjectConstructor - * untransformed representation of JSON_OBJECT() constructor diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index e1aadc39cf..4104371bd9 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1613,7 +1613,10 @@ typedef enum JsonConstructorType JSCTOR_JSON_OBJECT = 1, JSCTOR_JSON_ARRAY = 2, JSCTOR_JSON_OBJECTAGG = 3, - JSCTOR_JSON_ARRAYAGG = 4 + JSCTOR_JSON_ARRAYAGG = 4, + JSCTOR_JSON_SCALAR = 5, + JSCTOR_JSON_SERIALIZE = 6, + JSCTOR_JSON_PARSE = 7 } JsonConstructorType; /* diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index f5b2e61ca5..5984dcfa4b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -230,11 +230,13 @@ PG_KEYWORD("is", IS, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("isnull", ISNULL, TYPE_FUNC_NAME_KEYWORD, AS_LABEL) PG_KEYWORD("isolation", ISOLATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) -PG_KEYWORD("json", JSON, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index 649a1644f2..f5c7441f50 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -368,7 +368,6 @@ typedef struct JsonbIterator struct JsonbIterator *parent; } JsonbIterator; - /* Convenience macros */ static inline Jsonb * DatumGetJsonbP(Datum d) diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index 2ad648d1b8..c677ac8ff7 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -86,6 +86,6 @@ extern Datum datum_to_json(Datum val, JsonTypeCategory tcategory, Oid outfuncoid); extern Datum datum_to_jsonb(Datum val, JsonTypeCategory tcategory, Oid outfuncoid); -extern Datum jsonb_from_text(text *js); +extern Datum jsonb_from_text(text *js, bool unique_keys); #endif diff --git a/src/interfaces/ecpg/test/expected/sql-sqljson.c b/src/interfaces/ecpg/test/expected/sql-sqljson.c index a2c49b54f9..39221f9ea5 100644 --- a/src/interfaces/ecpg/test/expected/sql-sqljson.c +++ b/src/interfaces/ecpg/test/expected/sql-sqljson.c @@ -196,6 +196,202 @@ if (sqlca.sqlcode < 0) sqlprint();} printf("Found json=%s\n", json); + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( null )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 42 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 42 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( '{ \"a\" : 1 } ' format json )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 45 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 45 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( '{ \"a\" : 1 } ' format json encoding UTF8 )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 48 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 48 "sqljson.pgc" + + // error + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( ' 1 ' :: jsonb )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 51 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 51 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( ' 1 ' :: json with unique keys ) into json", ECPGt_EOIT, ECPGt_EORT); +#line 54 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 54 "sqljson.pgc" + + // error + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( '{\"a\": 1, \"a\": 2}' )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 57 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 57 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( '{\"a\": 1, \"a\": 2}' with unique keys )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 60 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 60 "sqljson.pgc" + + // error + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json_scalar ( null )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 63 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 63 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json_scalar ( null :: int )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 66 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 66 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json_scalar ( 123.45 )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 69 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 69 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json_scalar ( true )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 72 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 72 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json_scalar ( ' 123.45' )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 75 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 75 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json_scalar ( '2020-06-07 01:02:03' :: timestamp )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 78 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 78 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json_scalar ( '{}' :: jsonb )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 81 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 81 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json_serialize ( null )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 84 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 84 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json_serialize ( json ( '{ \"a\" : 1 } ' ) )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 87 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 87 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json_serialize ( '{ \"a\" : 1 } ' )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 90 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 90 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json_serialize ( '1' format json )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 93 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 93 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json_serialize ( '{ \"a\" : 1 } ' returning varchar )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 96 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 96 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json_serialize ( '{ \"a\" : 1 } ' returning jsonb )", ECPGt_EOIT, ECPGt_EORT); +#line 99 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 99 "sqljson.pgc" + + // error + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "with val ( js ) as ( values ( '{ \"a\": 1, \"b\": [{ \"a\": 1, \"b\": 0, \"a\": 2 }] }' ) ) select js is json \"IS JSON\" , js is not json \"IS NOT JSON\" , js is json value \"IS VALUE\" , js is json object \"IS OBJECT\" , js is json array \"IS ARRAY\" , js is json scalar \"IS SCALAR\" , js is json without unique keys \"WITHOUT UNIQUE\" , js is json with unique keys \"WITH UNIQUE\" from val", ECPGt_EOIT, ECPGt_bool,&(is_json[0]),(long)1,(long)1,sizeof(bool), ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, @@ -213,19 +409,19 @@ if (sqlca.sqlcode < 0) sqlprint();} ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_bool,&(is_json[7]),(long)1,(long)1,sizeof(bool), ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); -#line 54 "sqljson.pgc" +#line 114 "sqljson.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 54 "sqljson.pgc" +#line 114 "sqljson.pgc" for (int i = 0; i < sizeof(is_json); i++) printf("Found is_json[%d]: %s\n", i, is_json[i] ? "true" : "false"); { ECPGdisconnect(__LINE__, "CURRENT"); -#line 58 "sqljson.pgc" +#line 118 "sqljson.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 58 "sqljson.pgc" +#line 118 "sqljson.pgc" return 0; diff --git a/src/interfaces/ecpg/test/expected/sql-sqljson.stderr b/src/interfaces/ecpg/test/expected/sql-sqljson.stderr index 1252cb3b66..37a361156f 100644 --- a/src/interfaces/ecpg/test/expected/sql-sqljson.stderr +++ b/src/interfaces/ecpg/test/expected/sql-sqljson.stderr @@ -65,27 +65,208 @@ SQL error: duplicate JSON key "1" on line 33 [NO_PID]: sqlca: code: 0, state: 00000 [NO_PID]: ecpg_get_data on line 39: RESULT: {"1": 1} offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 42: query: with val ( js ) as ( values ( '{ "a": 1, "b": [{ "a": 1, "b": 0, "a": 2 }] }' ) ) select js is json "IS JSON" , js is not json "IS NOT JSON" , js is json value "IS VALUE" , js is json object "IS OBJECT" , js is json array "IS ARRAY" , js is json scalar "IS SCALAR" , js is json without unique keys "WITHOUT UNIQUE" , js is json with unique keys "WITH UNIQUE" from val; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_execute on line 42: query: select json ( null ); with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 [NO_PID]: ecpg_execute on line 42: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 42: correctly got 1 tuples with 8 fields +[NO_PID]: ecpg_process_output on line 42: correctly got 1 tuples with 1 fields [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 42: RESULT: t offset: -1; array: no +[NO_PID]: ecpg_get_data on line 42: RESULT: offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 42: RESULT: f offset: -1; array: no +[NO_PID]: raising sqlcode -213 on line 42: null value without indicator on line 42 +[NO_PID]: sqlca: code: -213, state: 22002 +SQL error: null value without indicator on line 42 +[NO_PID]: ecpg_execute on line 45: query: select json ( '{ "a" : 1 } ' format json ); with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 42: RESULT: t offset: -1; array: no +[NO_PID]: ecpg_execute on line 45: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 42: RESULT: t offset: -1; array: no +[NO_PID]: ecpg_process_output on line 45: correctly got 1 tuples with 1 fields [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 42: RESULT: f offset: -1; array: no +[NO_PID]: ecpg_get_data on line 45: RESULT: { "a" : 1 } offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 42: RESULT: f offset: -1; array: no +[NO_PID]: ecpg_execute on line 48: query: select json ( '{ "a" : 1 } ' format json encoding UTF8 ); with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 42: RESULT: t offset: -1; array: no +[NO_PID]: ecpg_execute on line 48: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 42: RESULT: f offset: -1; array: no +[NO_PID]: ecpg_check_PQresult on line 48: bad response - ERROR: JSON ENCODING clause is only allowed for bytea input type +LINE 1: select json ( '{ "a" : 1 } ' format json encoding UTF8 ) + ^ +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: raising sqlstate 42804 (sqlcode -400): JSON ENCODING clause is only allowed for bytea input type on line 48 +[NO_PID]: sqlca: code: -400, state: 42804 +SQL error: JSON ENCODING clause is only allowed for bytea input type on line 48 +[NO_PID]: ecpg_execute on line 51: query: select json ( ' 1 ' :: jsonb ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 51: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 51: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 51: RESULT: 1 offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 54: query: select json ( ' 1 ' :: json with unique keys ) into json; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 54: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_check_PQresult on line 54: bad response - ERROR: cannot use non-string types with WITH UNIQUE KEYS clause +LINE 1: select json ( ' 1 ' :: json with unique keys ) into json + ^ +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: raising sqlstate 42804 (sqlcode -400): cannot use non-string types with WITH UNIQUE KEYS clause on line 54 +[NO_PID]: sqlca: code: -400, state: 42804 +SQL error: cannot use non-string types with WITH UNIQUE KEYS clause on line 54 +[NO_PID]: ecpg_execute on line 57: query: select json ( '{"a": 1, "a": 2}' ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 57: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 57: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 57: RESULT: {"a": 1, "a": 2} offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 60: query: select json ( '{"a": 1, "a": 2}' with unique keys ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 60: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_check_PQresult on line 60: bad response - ERROR: duplicate JSON object key value +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: raising sqlstate 22030 (sqlcode -400): duplicate JSON object key value on line 60 +[NO_PID]: sqlca: code: -400, state: 22030 +SQL error: duplicate JSON object key value on line 60 +[NO_PID]: ecpg_execute on line 63: query: select json_scalar ( null ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 63: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 63: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 63: RESULT: offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: raising sqlcode -213 on line 63: null value without indicator on line 63 +[NO_PID]: sqlca: code: -213, state: 22002 +SQL error: null value without indicator on line 63 +[NO_PID]: ecpg_execute on line 66: query: select json_scalar ( null :: int ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 66: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 66: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 66: RESULT: offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: raising sqlcode -213 on line 66: null value without indicator on line 66 +[NO_PID]: sqlca: code: -213, state: 22002 +SQL error: null value without indicator on line 66 +[NO_PID]: ecpg_execute on line 69: query: select json_scalar ( 123.45 ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 69: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 69: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 69: RESULT: 123.45 offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 72: query: select json_scalar ( true ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 72: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 72: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 72: RESULT: true offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 75: query: select json_scalar ( ' 123.45' ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 75: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 75: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 75: RESULT: " 123.45" offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 78: query: select json_scalar ( '2020-06-07 01:02:03' :: timestamp ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 78: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 78: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 78: RESULT: "2020-06-07T01:02:03" offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 81: query: select json_scalar ( '{}' :: jsonb ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 81: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 81: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 81: RESULT: {} offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 84: query: select json_serialize ( null ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 84: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 84: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 84: RESULT: offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: raising sqlcode -213 on line 84: null value without indicator on line 84 +[NO_PID]: sqlca: code: -213, state: 22002 +SQL error: null value without indicator on line 84 +[NO_PID]: ecpg_execute on line 87: query: select json_serialize ( json ( '{ "a" : 1 } ' ) ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 87: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 87: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 87: RESULT: { "a" : 1 } offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 90: query: select json_serialize ( '{ "a" : 1 } ' ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 90: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 90: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 90: RESULT: { "a" : 1 } offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 93: query: select json_serialize ( '1' format json ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 93: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 93: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 93: RESULT: 1 offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 96: query: select json_serialize ( '{ "a" : 1 } ' returning varchar ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 96: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 96: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 96: RESULT: { "a" : 1 } offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 99: query: select json_serialize ( '{ "a" : 1 } ' returning jsonb ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 99: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_check_PQresult on line 99: bad response - ERROR: cannot use RETURNING type jsonb in JSON_SERIALIZE() +HINT: Try returning a string type or bytea. +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: raising sqlstate 42804 (sqlcode -400): cannot use RETURNING type jsonb in JSON_SERIALIZE() on line 99 +[NO_PID]: sqlca: code: -400, state: 42804 +SQL error: cannot use RETURNING type jsonb in JSON_SERIALIZE() on line 99 +[NO_PID]: ecpg_execute on line 102: query: with val ( js ) as ( values ( '{ "a": 1, "b": [{ "a": 1, "b": 0, "a": 2 }] }' ) ) select js is json "IS JSON" , js is not json "IS NOT JSON" , js is json value "IS VALUE" , js is json object "IS OBJECT" , js is json array "IS ARRAY" , js is json scalar "IS SCALAR" , js is json without unique keys "WITHOUT UNIQUE" , js is json with unique keys "WITH UNIQUE" from val; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 102: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 102: correctly got 1 tuples with 8 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 102: RESULT: t offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 102: RESULT: f offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 102: RESULT: t offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 102: RESULT: t offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 102: RESULT: f offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 102: RESULT: f offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 102: RESULT: t offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 102: RESULT: f offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 [NO_PID]: ecpg_finish: connection ecpg1_regression closed [NO_PID]: sqlca: code: 0, state: 00000 diff --git a/src/interfaces/ecpg/test/expected/sql-sqljson.stdout b/src/interfaces/ecpg/test/expected/sql-sqljson.stdout index 558901c406..83f8df13e5 100644 --- a/src/interfaces/ecpg/test/expected/sql-sqljson.stdout +++ b/src/interfaces/ecpg/test/expected/sql-sqljson.stdout @@ -4,6 +4,22 @@ Found json=[] Found json=[] Found json={"1" : 1, "1" : "2"} Found json={"1": 1} +Found json={"1": 1} +Found json={ "a" : 1 } +Found json=1 +Found json={"a": 1, "a": 2} +Found json={"a": 1, "a": 2} +Found json={"a": 1, "a": 2} +Found json=123.45 +Found json=true +Found json=" 123.45" +Found json="2020-06-07T01:02:03" +Found json={} +Found json={} +Found json={ "a" : 1 } +Found json={ "a" : 1 } +Found json=1 +Found json={ "a" : 1 } Found is_json[0]: true Found is_json[1]: false Found is_json[2]: true diff --git a/src/interfaces/ecpg/test/sql/sqljson.pgc b/src/interfaces/ecpg/test/sql/sqljson.pgc index a005503834..ddcbcc3b3c 100644 --- a/src/interfaces/ecpg/test/sql/sqljson.pgc +++ b/src/interfaces/ecpg/test/sql/sqljson.pgc @@ -39,6 +39,66 @@ EXEC SQL END DECLARE SECTION; EXEC SQL SELECT JSON_OBJECT(1: 1, '2': NULL ABSENT ON NULL WITHOUT UNIQUE RETURNING jsonb) INTO :json; printf("Found json=%s\n", json); + EXEC SQL SELECT JSON(NULL) INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON('{ "a" : 1 } ' FORMAT JSON) INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8) INTO :json; + // error + + EXEC SQL SELECT JSON(' 1 '::jsonb) INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON(' 1 '::json WITH UNIQUE KEYS) INTO json; + // error + + EXEC SQL SELECT JSON('{"a": 1, "a": 2}') INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS) INTO :json; + // error + + EXEC SQL SELECT JSON_SCALAR(NULL) INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON_SCALAR(NULL::int) INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON_SCALAR(123.45) INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON_SCALAR(true) INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON_SCALAR(' 123.45') INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp) INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON_SCALAR('{}'::jsonb) INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON_SERIALIZE(NULL) INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')) INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON_SERIALIZE('{ "a" : 1 } ') INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON_SERIALIZE('1' FORMAT JSON) INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar) INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); + // error + EXEC SQL WITH val (js) AS (VALUES ('{ "a": 1, "b": [{ "a": 1, "b": 0, "a": 2 }] }')) SELECT js IS JSON "IS JSON", diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index d73c7e2c6c..d5074d73a2 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -1,3 +1,293 @@ +-- JSON() +SELECT JSON(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON(); + ^ +SELECT JSON(NULL); + json +------ + +(1 row) + +SELECT JSON('{ "a" : 1 } '); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +ERROR: JSON ENCODING clause is only allowed for bytea input type +LINE 1: SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); + ^ +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); + json +-------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + pg_typeof +----------- + json +(1 row) + +SELECT JSON(' 1 '::json); + json +--------- + 1 +(1 row) + +SELECT JSON(' 1 '::jsonb); + json +------ + 1 +(1 row) + +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +ERROR: cannot use non-string types with WITH UNIQUE KEYS clause +LINE 1: SELECT JSON(' 1 '::json WITH UNIQUE KEYS); + ^ +SELECT JSON(123); +ERROR: cannot cast type integer to json +LINE 1: SELECT JSON(123); + ^ +SELECT JSON('{"a": 1, "a": 2}'); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +ERROR: duplicate JSON object key value +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + json +------------------ + {"a": 1, "a": 2} +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); + QUERY PLAN +----------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); + QUERY PLAN +------------------------------------------------------------- + Result + Output: JSON('\x313233'::bytea FORMAT JSON ENCODING UTF8) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); + QUERY PLAN +---------------------------------------------- + Result + Output: JSON('123'::text WITH UNIQUE KEYS) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); + QUERY PLAN +----------------------------- + Result + Output: JSON('123'::json) +(2 rows) + +SELECT pg_typeof(JSON('123')); + pg_typeof +----------- + json +(1 row) + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SCALAR(); + ^ +SELECT JSON_SCALAR(NULL); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(NULL::int); + json_scalar +------------- + +(1 row) + +SELECT JSON_SCALAR(123); + json_scalar +------------- + 123 +(1 row) + +SELECT JSON_SCALAR(123.45); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(123.45::numeric); + json_scalar +------------- + 123.45 +(1 row) + +SELECT JSON_SCALAR(true); + json_scalar +------------- + true +(1 row) + +SELECT JSON_SCALAR(false); + json_scalar +------------- + false +(1 row) + +SELECT JSON_SCALAR(' 123.45'); + json_scalar +------------- + " 123.45" +(1 row) + +SELECT JSON_SCALAR('2020-06-07'::date); + json_scalar +-------------- + "2020-06-07" +(1 row) + +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); + json_scalar +----------------------- + "2020-06-07T01:02:03" +(1 row) + +SELECT JSON_SCALAR('{}'::json); + json_scalar +------------- + {} +(1 row) + +SELECT JSON_SCALAR('{}'::jsonb); + json_scalar +------------- + {} +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); + QUERY PLAN +---------------------------- + Result + Output: JSON_SCALAR(123) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); + QUERY PLAN +------------------------------------ + Result + Output: JSON_SCALAR('123'::text) +(2 rows) + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +ERROR: syntax error at or near ")" +LINE 1: SELECT JSON_SERIALIZE(); + ^ +SELECT JSON_SERIALIZE(NULL); + json_serialize +---------------- + +(1 row) + +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } '); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT JSON_SERIALIZE('1'); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('1' FORMAT JSON); + json_serialize +---------------- + 1 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); + json_serialize +---------------------------- + \x7b20226122203a2031207d20 +(1 row) + +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); + json_serialize +---------------- + { "a" : 1 } +(1 row) + +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + pg_typeof +----------- + text +(1 row) + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); +ERROR: cannot use RETURNING type jsonb in JSON_SERIALIZE() +HINT: Try returning a string type or bytea. +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); + QUERY PLAN +----------------------------------------------------- + Result + Output: JSON_SERIALIZE('{}'::json RETURNING text) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + QUERY PLAN +------------------------------------------------------ + Result + Output: JSON_SERIALIZE('{}'::json RETURNING bytea) +(2 rows) + -- JSON_OBJECT() SELECT JSON_OBJECT(); json_object @@ -630,6 +920,13 @@ ERROR: duplicate JSON object key SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); ERROR: duplicate JSON object key +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + json_objectagg +------------------ + {"1": 1, "2": 2} +(1 row) + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -645,6 +942,41 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); CREATE OR REPLACE VIEW public.json_object_view AS SELECT JSON_OBJECT('foo' : '1'::text FORMAT JSON, 'bar' : 'baz'::text RETURNING json) AS "json_object" DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + a | json_objectagg +---------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(2 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); +ERROR: duplicate JSON key "1" +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1 } + {"k":1,"v":null} | { "1" : 1 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + a | json_objectagg +------------------+---------------------- + {"k":1,"v":1} | { "1" : 1, "2" : 2 } + {"k":1,"v":null} | { "1" : 1, "2" : 2 } + {"k":2,"v":2} | { "1" : 1, "2" : 2 } +(3 rows) + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 4fd820fd51..e6e20175b0 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -1,3 +1,67 @@ +-- JSON() +SELECT JSON(); +SELECT JSON(NULL); +SELECT JSON('{ "a" : 1 } '); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON); +SELECT JSON('{ "a" : 1 } ' FORMAT JSON ENCODING UTF8); +SELECT JSON('{ "a" : 1 } '::bytea FORMAT JSON ENCODING UTF8); +SELECT pg_typeof(JSON('{ "a" : 1 } ')); + +SELECT JSON(' 1 '::json); +SELECT JSON(' 1 '::jsonb); +SELECT JSON(' 1 '::json WITH UNIQUE KEYS); +SELECT JSON(123); + +SELECT JSON('{"a": 1, "a": 2}'); +SELECT JSON('{"a": 1, "a": 2}' WITH UNIQUE KEYS); +SELECT JSON('{"a": 1, "a": 2}' WITHOUT UNIQUE KEYS); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'::bytea FORMAT JSON ENCODING UTF8); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITH UNIQUE KEYS); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123' WITHOUT UNIQUE KEYS); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON('123'); +SELECT pg_typeof(JSON('123')); + +-- JSON_SCALAR() +SELECT JSON_SCALAR(); +SELECT JSON_SCALAR(NULL); +SELECT JSON_SCALAR(NULL::int); +SELECT JSON_SCALAR(123); +SELECT JSON_SCALAR(123.45); +SELECT JSON_SCALAR(123.45::numeric); +SELECT JSON_SCALAR(true); +SELECT JSON_SCALAR(false); +SELECT JSON_SCALAR(' 123.45'); +SELECT JSON_SCALAR('2020-06-07'::date); +SELECT JSON_SCALAR('2020-06-07 01:02:03'::timestamp); +SELECT JSON_SCALAR('{}'::json); +SELECT JSON_SCALAR('{}'::jsonb); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR(123); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SCALAR('123'); + +-- JSON_SERIALIZE() +SELECT JSON_SERIALIZE(); +SELECT JSON_SERIALIZE(NULL); +SELECT JSON_SERIALIZE(JSON('{ "a" : 1 } ')); +SELECT JSON_SERIALIZE('{ "a" : 1 } '); +SELECT JSON_SERIALIZE('1'); +SELECT JSON_SERIALIZE('1' FORMAT JSON); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING bytea); +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING varchar); +SELECT pg_typeof(JSON_SERIALIZE(NULL)); + +-- only string types or bytea allowed +SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb); + + +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}'); +EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); + -- JSON_OBJECT() SELECT JSON_OBJECT(); SELECT JSON_OBJECT(RETURNING json); @@ -216,6 +280,9 @@ FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) FROM (VALUES (1, 1), (1, NULL), (2, 2)) foo(k, v); +SELECT JSON_OBJECTAGG(k: v ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb) +FROM (VALUES (1, 1), (0, NULL),(4, null), (5, null),(6, null),(2, 2)) foo(k, v); + -- Test JSON_OBJECT deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); @@ -227,6 +294,24 @@ SELECT JSON_OBJECT('foo' : '1' FORMAT JSON, 'bar' : 'baz' RETURNING json); DROP VIEW json_object_view; +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v WITH UNIQUE KEYS) OVER (ORDER BY k) +FROM (VALUES (1,1), (1,2), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL WITH UNIQUE KEYS) + OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + +SELECT to_json(a) AS a, JSON_OBJECTAGG(k : v ABSENT ON NULL) +OVER (ORDER BY k RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +FROM (VALUES (1,1), (1,null), (2,2)) a(k,v); + -- Test JSON_ARRAY deparsing EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY('1' FORMAT JSON, 2 RETURNING json); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 05814136c6..e746a481d6 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1274,6 +1274,7 @@ JsonManifestWALRangeField JsonObjectAgg JsonObjectConstructor JsonOutput +JsonParseExpr JsonParseContext JsonParseErrorType JsonPath @@ -1294,8 +1295,11 @@ JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback JsonPathString +JsonQuotes JsonReturning +JsonScalarExpr JsonSemAction +JsonSerializeExpr JsonTokenType JsonTransformStringValuesAction JsonTypeCategory -- 2.35.3
v10-0005-JSON_TABLE.patch (application/octet-stream, 164.2K) download From 37b12ee82eff08a2fd3baf451e5211833f883b7c Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Tue, 18 Jul 2023 17:58:39 +0900 Subject: [PATCH v10 5/6] JSON_TABLE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This feature allows jsonb data to be treated as a table and thus used in a FROM clause like other tabular data. Data can be selected from the jsonb using jsonpath expressions, and hoisted out of nested structures in the jsonb to form multiple rows, more or less like an outer join. This also adds the PLAN clauses for JSON_TABLE, which allow the user to specify how data from nested paths are joined, allowing considerable freedom in shaping the tabular output of JSON_TABLE. PLAN DEFAULT allows the user to specify the global strategies when dealing with sibling or child nested paths. The is often sufficient to achieve the necessary goal, and is considerably simpler than the full PLAN clause, which allows the user to specify the strategy to be used for each named nested path. Author: Nikita Glukhov <n.gluhov@postgrespro.ru> Author: Teodor Sigaev <teodor@sigaev.ru> Author: Oleg Bartunov <obartunov@gmail.com> Author: Alexander Korotkov <aekorotkov@gmail.com> Author: Andrew Dunstan <andrew@dunslane.net> Author: Amit Langote <amitlangote09@gmail.com> Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- doc/src/sgml/func.sgml | 496 +++++++++ src/backend/commands/explain.c | 8 +- src/backend/executor/execExprInterp.c | 5 + src/backend/executor/nodeTableFuncscan.c | 27 +- src/backend/nodes/makefuncs.c | 19 + src/backend/nodes/nodeFuncs.c | 30 + src/backend/parser/Makefile | 1 + src/backend/parser/gram.y | 476 ++++++++- src/backend/parser/meson.build | 1 + src/backend/parser/parse_clause.c | 14 +- src/backend/parser/parse_expr.c | 35 +- src/backend/parser/parse_jsontable.c | 741 +++++++++++++ src/backend/parser/parse_relation.c | 5 +- src/backend/parser/parse_target.c | 3 + src/backend/utils/adt/jsonpath_exec.c | 543 ++++++++++ src/backend/utils/adt/ruleutils.c | 279 ++++- src/include/nodes/execnodes.h | 2 + src/include/nodes/makefuncs.h | 2 + src/include/nodes/parsenodes.h | 90 ++ src/include/nodes/primnodes.h | 61 +- src/include/parser/kwlist.h | 4 + src/include/parser/parse_clause.h | 3 + src/include/utils/jsonpath.h | 3 + src/test/regress/expected/json_sqljson.out | 6 + src/test/regress/expected/jsonb_sqljson.out | 1069 +++++++++++++++++++ src/test/regress/sql/json_sqljson.sql | 4 + src/test/regress/sql/jsonb_sqljson.sql | 629 +++++++++++ src/tools/pgindent/typedefs.list | 13 + 28 files changed, 4536 insertions(+), 33 deletions(-) create mode 100644 src/backend/parser/parse_jsontable.c diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index cc16006628..f57026d5a9 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -17137,6 +17137,502 @@ array w/o UK? | t </sect2> + <sect2 id="functions-sqljson-table"> + <title>JSON_TABLE</title> + <indexterm> + <primary>json_table</primary> + </indexterm> + + <para> + <function>json_table</function> is an SQL/JSON function which + queries <acronym>JSON</acronym> data + and presents the results as a relational view, which can be accessed as a + regular SQL table. You can only use <function>json_table</function> inside the + <literal>FROM</literal> clause of a <literal>SELECT</literal> statement. + </para> + + <para> + Taking JSON data as input, <function>json_table</function> uses + a path expression to extract a part of the provided data that + will be used as a <firstterm>row pattern</firstterm> for the + constructed view. Each SQL/JSON item at the top level of the row pattern serves + as the source for a separate row in the constructed relational view. + </para> + + <para> + To split the row pattern into columns, <function>json_table</function> + provides the <literal>COLUMNS</literal> clause that defines the + schema of the created view. For each column to be constructed, + this clause provides a separate path expression that evaluates + the row pattern, extracts a JSON item, and returns it as a + separate SQL value for the specified column. If the required value + is stored in a nested level of the row pattern, it can be extracted + using the <literal>NESTED PATH</literal> subclause. Joining the + columns returned by <literal>NESTED PATH</literal> can add multiple + new rows to the constructed view. Such rows are called + <firstterm>child rows</firstterm>, as opposed to the <firstterm>parent row</firstterm> + that generates them. + </para> + + <para> + The rows produced by <function>JSON_TABLE</function> are laterally + joined to the row that generated them, so you do not have to explicitly join + the constructed view with the original table holding <acronym>JSON</acronym> + data. Optionally, you can specify how to join the columns returned + by <literal>NESTED PATH</literal> using the <literal>PLAN</literal> clause. + </para> + + <para> + Each <literal>NESTED PATH</literal> clause can generate one or more + columns. Columns produced by <literal>NESTED PATH</literal>s at the + same level are considered to be <firstterm>siblings</firstterm>, + while a column produced by a <literal>NESTED PATH</literal> is + considered to be a child of the column produced by a + <literal>NESTED PATH</literal> or row expression at a higher level. + Sibling columns are always joined first. Once they are processed, + the resulting rows are joined to the parent row. + </para> + + <para> + The syntax is: + </para> + +<synopsis> +JSON_TABLE ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> AS <replaceable>json_path_name</replaceable> </optional> <optional> PASSING { <replaceable>value</replaceable> AS <replaceable>varname</replaceable> } <optional>, ...</optional> </optional> + COLUMNS ( <replaceable class="parameter">json_table_column</replaceable> <optional>, ...</optional> ) + <optional> { <literal>ERROR</literal> | <literal>EMPTY</literal> } <literal>ON ERROR</literal> </optional> + <optional> + PLAN ( <replaceable class="parameter">json_table_plan</replaceable> ) | + PLAN DEFAULT ( { INNER | OUTER } <optional> , { CROSS | UNION } </optional> + | { CROSS | UNION } <optional> , { INNER | OUTER } </optional> ) + </optional> +) + +<phrase> +where <replaceable class="parameter">json_table_column</replaceable> is: +</phrase> + <replaceable>name</replaceable> <replaceable>type</replaceable> <optional> PATH <replaceable>json_path_specification</replaceable> </optional> + <optional> { WITHOUT | WITH { CONDITIONAL | <optional>UNCONDITIONAL</optional> } } <optional> ARRAY </optional> WRAPPER </optional> + <optional> { KEEP | OMIT } QUOTES <optional> ON SCALAR STRING </optional> </optional> + <optional> { ERROR | NULL | DEFAULT <replaceable>expression</replaceable> } ON EMPTY </optional> + <optional> { ERROR | NULL | DEFAULT <replaceable>expression</replaceable> } ON ERROR </optional> + | <replaceable>name</replaceable> <replaceable>type</replaceable> FORMAT <replaceable>json_representation</replaceable> + <optional> PATH <replaceable>json_path_specification</replaceable> </optional> + <optional> { WITHOUT | WITH { CONDITIONAL | <optional>UNCONDITIONAL</optional> } } <optional> ARRAY </optional> WRAPPER </optional> + <optional> { KEEP | OMIT } QUOTES <optional> ON SCALAR STRING </optional> </optional> + <optional> { ERROR | NULL | EMPTY { ARRAY | OBJECT } | DEFAULT <replaceable>expression</replaceable> } ON EMPTY </optional> + <optional> { ERROR | NULL | EMPTY { ARRAY | OBJECT } | DEFAULT <replaceable>expression</replaceable> } ON ERROR </optional> + | <replaceable>name</replaceable> <replaceable>type</replaceable> EXISTS <optional> PATH <replaceable>json_path_specification</replaceable> </optional> + <optional> { ERROR | TRUE | FALSE | UNKNOWN } ON ERROR </optional> + | NESTED PATH <replaceable>json_path_specification</replaceable> <optional> AS <replaceable>path_name</replaceable> </optional> + COLUMNS ( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + | <replaceable>name</replaceable> FOR ORDINALITY +<phrase> +<replaceable>json_table_plan</replaceable> is: +</phrase> + <replaceable>json_path_name</replaceable> <optional> { OUTER | INNER } <replaceable>json_table_plan_primary</replaceable> </optional> + | <replaceable>json_table_plan_primary</replaceable> { UNION <replaceable>json_table_plan_primary</replaceable> } <optional>...</optional> + | <replaceable>json_table_plan_primary</replaceable> { CROSS <replaceable>json_table_plan_primary</replaceable> } <optional>...</optional> +<phrase> +<replaceable>json_table_plan_primary</replaceable> is: +</phrase> + <replaceable>json_path_name</replaceable> | ( <replaceable>json_table_plan</replaceable> ) +</synopsis> + + <para> + Each syntax element is described below in more detail. + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional></literal> + </term> + <listitem> + <para> + The input data to query, the JSON path expression defining the query, + and an optional <literal>PASSING</literal> clause, which can provide data + values to the <replaceable>path_expression</replaceable>. + The result of the input data + evaluation is called the <firstterm>row pattern</firstterm>. The row + pattern is used as the source for row values in the constructed view. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>COLUMNS</literal>( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + The <literal>COLUMNS</literal> clause defining the schema of the + constructed view. In this clause, you must specify all the columns + to be filled with SQL/JSON items. + The <replaceable>json_table_column</replaceable> + expression has the following syntax variants: + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>name</replaceable> <replaceable>type</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional></literal> + </term> + <listitem> + + <para> + Inserts a single SQL/JSON item into each row of + the specified column. + </para> + <para> + The provided <literal>PATH</literal> expression is evaluated and + the column is filled with the produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>ON EMPTY</literal> and + <literal>ON ERROR</literal> clauses to define how to handle missing values + or structural errors. + <literal>WRAPPER</literal> and <literal>QUOTES</literal> clauses can only + be used with JSON, array, and composite types. + These clauses have the same syntax and semantics as for + <function>json_value</function> and <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> <literal>FORMAT</literal> <replaceable>json_representation</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a composite SQL/JSON + item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression is evaluated and + the column is filled with the produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>WRAPPER</literal>, <literal>QUOTES</literal>, + <literal>ON EMPTY</literal> and <literal>ON ERROR</literal> clauses + to define additional settings for the returned SQL/JSON items. + These clauses have the same syntax and semantics as + for <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> + <literal>EXISTS</literal> <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a boolean item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression is evaluated, + a check whether any SQL/JSON items were returned is done, and + the column is filled with the resulting boolean value, one for each row. + The specified <parameter>type</parameter> should have a cast from the + <type>boolean</type>. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + </para> + <para> + Optionally, you can add <literal>ON ERROR</literal> clause to define + error behavior. This clause has the same syntax and semantics as + for <function>json_exists</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>NESTED PATH</literal> <replaceable>json_path_specification</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> + <literal>COLUMNS</literal> ( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + Extracts SQL/JSON items from nested levels of the row pattern, + generates one or more columns as defined by the <literal>COLUMNS</literal> + subclause, and inserts the extracted SQL/JSON items into each row of these columns. + The <replaceable>json_table_column</replaceable> expression in the + <literal>COLUMNS</literal> subclause uses the same syntax as in the + parent <literal>COLUMNS</literal> clause. + </para> + + <para> + The <literal>NESTED PATH</literal> syntax is recursive, + so you can go down multiple nested levels by specifying several + <literal>NESTED PATH</literal> subclauses within each other. + It allows to unnest the hierarchy of JSON objects and arrays + in a single function invocation rather than chaining several + <function>JSON_TABLE</function> expressions in an SQL statement. + </para> + + <para> + You can use the <literal>PLAN</literal> clause to define how + to join the columns returned by <literal>NESTED PATH</literal> clauses. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <literal>FOR ORDINALITY</literal> + </term> + <listitem> + + <para> + Adds an ordinality column that provides sequential row numbering. + You can have only one ordinality column per table. Row numbering + is 1-based. For child rows that result from the <literal>NESTED PATH</literal> + clauses, the parent row number is repeated. + </para> + </listitem> + </varlistentry> + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>AS</literal> <replaceable>json_path_name</replaceable> + </term> + <listitem> + + <para> + The optional <replaceable>json_path_name</replaceable> serves as an + identifier of the provided <replaceable>json_path_specification</replaceable>. + The path name must be unique and distinct from the column names. + When using the <literal>PLAN</literal> clause, you must specify the names + for all the paths, including the row pattern. Each path name can appear in + the <literal>PLAN</literal> clause only once. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN</literal> ( <replaceable>json_table_plan</replaceable> ) + </term> + <listitem> + + <para> + Defines how to join the data returned by <literal>NESTED PATH</literal> + clauses to the constructed view. + </para> + <para> + To join columns with parent/child relationship, you can use: + </para> + <variablelist> + <varlistentry> + <term> + <literal>INNER</literal> + </term> + <listitem> + + <para> + Use <literal>INNER JOIN</literal>, so that the parent row + is omitted from the output if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>OUTER</literal> + </term> + <listitem> + + <para> + Use <literal>LEFT OUTER JOIN</literal>, so that the parent row + is always included into the output even if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>, with NULL values + inserted into the child columns if the corresponding + values are missing. + </para> + <para> + This is the default option for joining columns with parent/child relationship. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para> + To join sibling columns, you can use: + </para> + + <variablelist> + <varlistentry> + <term> + <literal>UNION</literal> + </term> + <listitem> + + <para> + Generate one row for each value produced by each of the sibling + columns. The columns from the other siblings are set to null. + </para> + <para> + This is the default option for joining sibling columns. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>CROSS</literal> + </term> + <listitem> + + <para> + Generate one row for each combination of values from the sibling columns. + </para> + </listitem> + </varlistentry> + + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN DEFAULT</literal> ( <literal><replaceable>OUTER | INNER</replaceable> <optional>, <replaceable>UNION | CROSS</replaceable> </optional></literal> ) + </term> + <listitem> + <para> + The terms can also be specified in reverse order. The + <literal>INNER</literal> or <literal>OUTER</literal> option defines the + joining plan for parent/child columns, while <literal>UNION</literal> or + <literal>CROSS</literal> affects joins of sibling columns. This form + of <literal>PLAN</literal> overrides the default plan for + all columns at once. Even though the path names are not included in the + <literal>PLAN DEFAULT</literal> form, to conform to the SQL/JSON standard + they must be provided for all the paths if the <literal>PLAN</literal> + clause is used. + </para> + <para> + <literal>PLAN DEFAULT</literal> is simpler than specifying a complete + <literal>PLAN</literal>, and is often all that is required to get the desired + output. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para>Examples</para> + + <para> + In these examples the following small table storing some JSON data will be used: +<programlisting> +CREATE TABLE my_films ( js jsonb ); + +INSERT INTO my_films VALUES ( +'{ "favorites" : [ + { "kind" : "comedy", "films" : [ + { "title" : "Bananas", + "director" : "Woody Allen"}, + { "title" : "The Dinner Game", + "director" : "Francis Veber" } ] }, + { "kind" : "horror", "films" : [ + { "title" : "Psycho", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "thriller", "films" : [ + { "title" : "Vertigo", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "drama", "films" : [ + { "title" : "Yojimbo", + "director" : "Akira Kurosawa" } ] } + ] }'); +</programlisting> + </para> + <para> + Query the <structname>my_films</structname> table holding + some JSON data about the films and create a view that + distributes the film genre, title, and director between separate columns: +<screen> +SELECT jt.* FROM + my_films, + JSON_TABLE ( js, '$.favorites[*]' COLUMNS ( + id FOR ORDINALITY, + kind text PATH '$.kind', + NESTED PATH '$.films[*]' COLUMNS ( + title text PATH '$.title', + director text PATH '$.director'))) AS jt; +----+----------+------------------+------------------- + id | kind | title | director +----+----------+------------------+------------------- + 1 | comedy | Bananas | Woody Allen + 1 | comedy | The Dinner Game | Francis Veber + 2 | horror | Psycho | Alfred Hitchcock + 3 | thriller | Vertigo | Alfred Hitchcock + 4 | drama | Yojimbo | Akira Kurosawa + (5 rows) +</screen> + </para> + + <para> + Find a director that has done films in two different genres: +<screen> +SELECT + director1 AS director, title1, kind1, title2, kind2 +FROM + my_films, + JSON_TABLE ( js, '$.favorites' AS favs COLUMNS ( + NESTED PATH '$[*]' AS films1 COLUMNS ( + kind1 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film1 COLUMNS ( + title1 text PATH '$.title', + director1 text PATH '$.director') + ), + NESTED PATH '$[*]' AS films2 COLUMNS ( + kind2 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film2 COLUMNS ( + title2 text PATH '$.title', + director2 text PATH '$.director' + ) + ) + ) + PLAN (favs OUTER ((films1 INNER film1) CROSS (films2 INNER film2))) + ) AS jt + WHERE kind1 > kind2 AND director1 = director2; + + director | title1 | kind1 | title2 | kind2 +------------------+---------+----------+--------+-------- + Alfred Hitchcock | Vertigo | thriller | Psycho | horror +(1 row) +</screen> + </para> + </sect2> + <sect2 id="functions-sqljson-path"> <title>The SQL/JSON Path Language</title> diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 8570b14f62..ad899de5d6 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -3862,7 +3862,13 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es) break; case T_TableFuncScan: Assert(rte->rtekind == RTE_TABLEFUNC); - objectname = "xmltable"; + if (rte->tablefunc) + if (rte->tablefunc->functype == TFT_XMLTABLE) + objectname = "xmltable"; + else /* Must be TFT_JSON_TABLE */ + objectname = "json_table"; + else + objectname = NULL; objecttag = "Table Function Name"; break; case T_ValuesScan: diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 1622e44b12..6774e98dbb 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -4309,6 +4309,11 @@ ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) break; } + case JSON_TABLE_OP: + res = item; + resnull = false; + break; + default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); *op->resnull = true; diff --git a/src/backend/executor/nodeTableFuncscan.c b/src/backend/executor/nodeTableFuncscan.c index 791cbd2372..085a612533 100644 --- a/src/backend/executor/nodeTableFuncscan.c +++ b/src/backend/executor/nodeTableFuncscan.c @@ -28,6 +28,7 @@ #include "miscadmin.h" #include "nodes/execnodes.h" #include "utils/builtins.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/xml.h" @@ -161,8 +162,9 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) scanstate->ss.ps.qual = ExecInitQual(node->scan.plan.qual, &scanstate->ss.ps); - /* Only XMLTABLE is supported currently */ - scanstate->routine = &XmlTableRoutine; + /* Only XMLTABLE and JSON_TABLE are supported currently */ + scanstate->routine = + tf->functype == TFT_XMLTABLE ? &XmlTableRoutine : &JsonbTableRoutine; scanstate->perTableCxt = AllocSetContextCreate(CurrentMemoryContext, @@ -182,6 +184,10 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) ExecInitExprList(tf->colexprs, (PlanState *) scanstate); scanstate->coldefexprs = ExecInitExprList(tf->coldefexprs, (PlanState *) scanstate); + scanstate->colvalexprs = + ExecInitExprList(tf->colvalexprs, (PlanState *) scanstate); + scanstate->passingvalexprs = + ExecInitExprList(tf->passingvalexprs, (PlanState *) scanstate); scanstate->notnulls = tf->notnulls; @@ -381,14 +387,17 @@ tfuncInitialize(TableFuncScanState *tstate, ExprContext *econtext, Datum doc) routine->SetNamespace(tstate, ns_name, ns_uri); } - /* Install the row filter expression into the table builder context */ - value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); - if (isnull) - ereport(ERROR, - (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), - errmsg("row filter expression must not be null"))); + if (routine->SetRowFilter) + { + /* Install the row filter expression into the table builder context */ + value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); + if (isnull) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("row filter expression must not be null"))); - routine->SetRowFilter(tstate, TextDatumGetCString(value)); + routine->SetRowFilter(tstate, TextDatumGetCString(value)); + } /* * Install the column filter expressions into the table builder context. diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 9c02634355..dd5e17c6e9 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -875,6 +875,25 @@ makeJsonBehavior(JsonBehaviorType type, Node *default_expr) return behavior; } +/* + * makeJsonTableJoinedPlan - + * creates a joined JsonTablePlan node + */ +Node * +makeJsonTableJoinedPlan(JsonTablePlanJoinType type, Node *plan1, Node *plan2, + int location) +{ + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_JOINED; + n->join_type = type; + n->plan1 = castNode(JsonTablePlan, plan1); + n->plan2 = castNode(JsonTablePlan, plan2); + n->location = location; + + return (Node *) n; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index cd5fff2bbe..64a4fbf093 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -2629,6 +2629,10 @@ expression_tree_walker_impl(Node *node, return true; if (WALK(tf->coldefexprs)) return true; + if (WALK(tf->colvalexprs)) + return true; + if (WALK(tf->passingvalexprs)) + return true; } break; default: @@ -3697,6 +3701,8 @@ expression_tree_mutator_impl(Node *node, MUTATE(newnode->rowexpr, tf->rowexpr, Node *); MUTATE(newnode->colexprs, tf->colexprs, List *); MUTATE(newnode->coldefexprs, tf->coldefexprs, List *); + MUTATE(newnode->colvalexprs, tf->colvalexprs, List *); + MUTATE(newnode->passingvalexprs, tf->passingvalexprs, List *); return (Node *) newnode; } break; @@ -4128,6 +4134,30 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonTable: + { + JsonTable *jt = (JsonTable *) node; + + if (WALK(jt->common)) + return true; + if (WALK(jt->columns)) + return true; + } + break; + case T_JsonTableColumn: + { + JsonTableColumn *jtc = (JsonTableColumn *) node; + + if (WALK(jtc->typeName)) + return true; + if (WALK(jtc->on_empty)) + return true; + if (WALK(jtc->on_error)) + return true; + if (jtc->coltype == JTC_NESTED && WALK(jtc->columns)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile index 9f1c4022bb..f4c0cc7f10 100644 --- a/src/backend/parser/Makefile +++ b/src/backend/parser/Makefile @@ -23,6 +23,7 @@ OBJS = \ parse_enr.o \ parse_expr.o \ parse_func.o \ + parse_jsontable.o \ parse_merge.o \ parse_node.o \ parse_oper.o \ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 1a1614645c..40e9275069 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -654,19 +654,43 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_aggregate_func json_api_common_syntax json_argument + json_table + json_table_column_definition + json_table_ordinality_column_definition + json_table_regular_column_definition + json_table_formatted_column_definition + json_table_exists_column_definition + json_table_nested_columns + json_table_plan_clause_opt + json_table_plan + json_table_plan_simple + json_table_plan_parent_child + json_table_plan_outer + json_table_plan_inner + json_table_plan_sibling + json_table_plan_union + json_table_plan_cross + json_table_plan_primary %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt json_arguments + json_table_columns_clause + json_table_column_definition_list +%type <str> json_table_column_path_specification_clause_opt %type <ival> json_encoding_clause_opt json_predicate_type_constraint json_wrapper_behavior + json_table_default_plan_choices + json_table_default_plan_inner_outer + json_table_default_plan_union_cross %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt %type <jsbehavior> json_value_behavior json_query_behavior json_exists_behavior + json_table_behavior %type <js_quotes> json_quotes_clause_opt @@ -733,7 +757,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG - JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_TABLE JSON_VALUE KEY KEYS KEEP @@ -744,8 +768,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE - NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE - NORMALIZE NORMALIZED + NAME_P NAMES NATIONAL NATURAL NCHAR NESTED NEW NEXT NFC NFD NFKC NFKD NO + NONE NORMALIZE NORMALIZED NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC @@ -753,8 +777,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER - PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD - PLACING PLANS POLICY + PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH + PLACING PLAN PLANS POLICY POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION @@ -862,6 +886,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); * Using the same precedence as IDENT seems right for the reasons given above. */ %nonassoc UNBOUNDED /* ideally would have same precedence as IDENT */ +%nonassoc COLUMNS %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' @@ -884,6 +909,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL +%nonassoc json_table_column +%nonassoc NESTED +%left PATH %% /* @@ -13334,6 +13362,21 @@ table_ref: relation_expr opt_alias_clause $2->alias = $4; $$ = (Node *) $2; } + | json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $1); + + jt->alias = $2; + $$ = (Node *) jt; + } + | LATERAL_P json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $2); + + jt->alias = $3; + jt->lateral = true; + $$ = (Node *) jt; + } ; @@ -13901,6 +13944,8 @@ xmltable_column_option_el: { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(true), @1); } | NULL_P { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(false), @1); } + | PATH b_expr + { $$ = makeDefElem("path", $2, @1); } ; xml_namespace_list: @@ -16708,6 +16753,11 @@ json_value_behavior: | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } ; +json_table_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + ; + /* ARRAY is a noise word */ json_wrapper_behavior: WITHOUT WRAPPER { $$ = JSW_NONE; } @@ -16729,6 +16779,414 @@ json_quotes_clause_opt: | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } ; +json_table: + JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + json_table_behavior ON ERROR_P + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_columns_clause: + COLUMNS '(' json_table_column_definition_list ')' { $$ = $3; } + ; + +json_table_column_definition_list: + json_table_column_definition + { $$ = list_make1($1); } + | json_table_column_definition_list ',' json_table_column_definition + { $$ = lappend($1, $3); } + ; + +json_table_column_definition: + json_table_ordinality_column_definition %prec json_table_column + | json_table_regular_column_definition %prec json_table_column + | json_table_formatted_column_definition %prec json_table_column + | json_table_exists_column_definition %prec json_table_column + | json_table_nested_columns + ; + +json_table_ordinality_column_definition: + ColId FOR ORDINALITY + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FOR_ORDINALITY; + n->name = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_regular_column_definition: + ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_exists_column_definition: + ColId Typename + EXISTS json_table_column_path_specification_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + EXISTS json_table_column_path_specification_clause_opt + json_exists_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_column_path_specification_clause_opt: + PATH Sconst { $$ = $2; } + | /* EMPTY */ %prec json_table_column { $$ = NULL; } + ; + +json_table_formatted_column_definition: + ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $8 == JS_QUOTES_OMIT; + n->on_empty = $9; + n->on_error = $12; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_nested_columns: + NESTED path_opt Sconst + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = NULL; + n->columns = $4; + n->location = @1; + $$ = (Node *) n; + } + | NESTED path_opt Sconst AS name + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = $5; + n->columns = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +path_opt: + PATH { } + | /* EMPTY */ { } + ; + +json_table_plan_clause_opt: + PLAN '(' json_table_plan ')' { $$ = $3; } + | PLAN DEFAULT '(' json_table_default_plan_choices ')' + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_DEFAULT; + n->join_type = $4; + n->location = @1; + $$ = (Node *) n; + } + | /* EMPTY */ { $$ = NULL; } + ; + +json_table_plan: + json_table_plan_simple + | json_table_plan_parent_child + | json_table_plan_sibling + ; + +json_table_plan_simple: + name + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_SIMPLE; + n->pathname = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_plan_primary: + json_table_plan_simple { $$ = $1; } + | '(' json_table_plan ')' + { + castNode(JsonTablePlan, $2)->location = @1; + $$ = $2; + } + ; + +json_table_plan_outer: + json_table_plan_simple OUTER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_OUTER, $1, $3, @1); } + ; + +json_table_plan_inner: + json_table_plan_simple INNER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_INNER, $1, $3, @1); } + ; + +json_table_plan_parent_child: + json_table_plan_outer + | json_table_plan_inner + ; + +json_table_plan_union: + json_table_plan_primary UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + | json_table_plan_union UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + ; + +json_table_plan_cross: + json_table_plan_primary CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + | json_table_plan_cross CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + ; + +json_table_plan_sibling: + json_table_plan_union + | json_table_plan_cross + ; + +json_table_default_plan_choices: + json_table_default_plan_inner_outer { $$ = $1 | JSTPJ_UNION; } + | json_table_default_plan_union_cross { $$ = $1 | JSTPJ_OUTER; } + | json_table_default_plan_inner_outer ',' + json_table_default_plan_union_cross { $$ = $1 | $3; } + | json_table_default_plan_union_cross ',' + json_table_default_plan_inner_outer { $$ = $1 | $3; } + ; + +json_table_default_plan_inner_outer: + INNER_P { $$ = JSTPJ_INNER; } + | OUTER_P { $$ = JSTPJ_OUTER; } + ; + +json_table_default_plan_union_cross: + UNION { $$ = JSTPJ_UNION; } + | CROSS { $$ = JSTPJ_CROSS; } + ; + json_returning_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17453,6 +17911,7 @@ unreserved_keyword: | MOVE | NAME_P | NAMES + | NESTED | NEW | NEXT | NFC @@ -17487,6 +17946,8 @@ unreserved_keyword: | PARTITION | PASSING | PASSWORD + | PATH + | PLAN | PLANS | POLICY | PRECEDING @@ -17651,6 +18112,7 @@ col_name_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | LEAST | NATIONAL @@ -18019,6 +18481,7 @@ bare_label_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | KEEP | KEY @@ -18058,6 +18521,7 @@ bare_label_keyword: | NATIONAL | NATURAL | NCHAR + | NESTED | NEW | NEXT | NFC @@ -18102,7 +18566,9 @@ bare_label_keyword: | PARTITION | PASSING | PASSWORD + | PATH | PLACING + | PLAN | PLANS | POLICY | POSITION diff --git a/src/backend/parser/meson.build b/src/backend/parser/meson.build index 94517a7f7d..0f0e4ac7ca 100644 --- a/src/backend/parser/meson.build +++ b/src/backend/parser/meson.build @@ -10,6 +10,7 @@ backend_sources += files( 'parse_enr.c', 'parse_expr.c', 'parse_func.c', + 'parse_jsontable.c', 'parse_merge.c', 'parse_node.c', 'parse_oper.c', diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index 334b9b42bd..affd812619 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -697,7 +697,11 @@ transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf) char **names; int colno; - /* Currently only XMLTABLE is supported */ + /* + * Currently we only support XMLTABLE here. See transformJsonTable() for + * JSON_TABLE support. + */ + tf->functype = TFT_XMLTABLE; constructName = "XMLTABLE"; docType = XMLOID; @@ -1104,13 +1108,17 @@ transformFromClauseItem(ParseState *pstate, Node *n, rtr->rtindex = nsitem->p_rtindex; return (Node *) rtr; } - else if (IsA(n, RangeTableFunc)) + else if (IsA(n, RangeTableFunc) || IsA(n, JsonTable)) { /* table function is like a plain relation */ RangeTblRef *rtr; ParseNamespaceItem *nsitem; - nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + if (IsA(n, RangeTableFunc)) + nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + else + nsitem = transformJsonTable(pstate, (JsonTable *) n); + *top_nsitem = nsitem; *namespace = list_make1(nsitem); rtr = makeNode(RangeTblRef); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index e8ac4eeaf3..4fdc8231e1 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4250,7 +4250,7 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) JsonFormatType format; char *constructName; - if (func->common->pathname) + if (func->common->pathname && func->op != JSON_TABLE_OP) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("JSON_TABLE path name is not allowed here"), @@ -4269,6 +4269,9 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) case JSON_EXISTS_OP: constructName = "JSON_EXISTS()"; break; + case JSON_TABLE_OP: + constructName = "JSON_TABLE()"; + break; default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); break; @@ -4308,14 +4311,19 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) &jsexpr->passing_values, &jsexpr->passing_names); - if (func->op != JSON_EXISTS_OP) + if (func->op != JSON_EXISTS_OP && func->op != JSON_TABLE_OP) jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, JSON_BEHAVIOR_NULL); - jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, - func->op == JSON_EXISTS_OP ? - JSON_BEHAVIOR_FALSE : - JSON_BEHAVIOR_NULL); + if (func->op == JSON_EXISTS_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_FALSE); + else if (func->op == JSON_TABLE_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_EMPTY); + else + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_NULL); return jsexpr; } @@ -4639,6 +4647,21 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->result_coercion->expr = NULL; } break; + + case JSON_TABLE_OP: + jsexpr->returning = makeNode(JsonReturning); + jsexpr->returning->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + jsexpr->returning->typid = exprType(contextItemExpr); + jsexpr->returning->typmod = -1; + + if (jsexpr->returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("JSON_TABLE() is not yet implemented for the json type"), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + break; } if (exprType(contextItemExpr) != JSONBOID) diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c new file mode 100644 index 0000000000..34e9f16f5c --- /dev/null +++ b/src/backend/parser/parse_jsontable.c @@ -0,0 +1,741 @@ +/*------------------------------------------------------------------------- + * + * parse_jsontable.c + * parsing of JSON_TABLE + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/parser/parse_jsontable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/pg_collation.h" +#include "catalog/pg_type.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "optimizer/optimizer.h" +#include "parser/parse_clause.h" +#include "parser/parse_collate.h" +#include "parser/parse_expr.h" +#include "parser/parse_relation.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/json.h" +#include "utils/lsyscache.h" + +/* Context for JSON_TABLE transformation */ +typedef struct JsonTableParseContext +{ + ParseState *pstate; /* parsing state */ + JsonTable *table; /* untransformed node */ + TableFunc *tablefunc; /* transformed node */ + List *pathNames; /* list of all path and columns names */ + int pathNameId; /* path name id counter */ + Oid contextItemTypid; /* type oid of context item (json/jsonb) */ +} JsonTableParseContext; + +static JsonTableParent *transformJsonTableColumns(JsonTableParseContext *cxt, + JsonTablePlan *plan, + List *columns, + char *pathSpec, + char **pathName, + int location); + +static Node * +makeStringConst(char *str, int location) +{ + A_Const *n = makeNode(A_Const); + + n->val.node.type = T_String; + n->val.sval.sval = str; + n->location = location; + + return (Node *) n; +} + +/* + * Transform JSON_TABLE column + * - regular column into JSON_VALUE() + * - FORMAT JSON column into JSON_QUERY() + * - EXISTS column into JSON_EXISTS() + */ +static Node * +transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, + List *passingArgs, bool errorOnError) +{ + JsonFuncExpr *jfexpr = makeNode(JsonFuncExpr); + JsonCommon *common = makeNode(JsonCommon); + JsonOutput *output = makeNode(JsonOutput); + char *pathspec; + JsonFormat *default_format; + + jfexpr->op = + jtc->coltype == JTC_REGULAR ? JSON_VALUE_OP : + jtc->coltype == JTC_EXISTS ? JSON_EXISTS_OP : JSON_QUERY_OP; + jfexpr->common = common; + jfexpr->output = output; + jfexpr->on_empty = jtc->on_empty; + jfexpr->on_error = jtc->on_error; + if (!jfexpr->on_error && errorOnError) + jfexpr->on_error = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); + jfexpr->omit_quotes = jtc->omit_quotes; + jfexpr->wrapper = jtc->wrapper; + jfexpr->location = jtc->location; + + output->typeName = jtc->typeName; + output->returning = makeNode(JsonReturning); + output->returning->format = jtc->format; + + default_format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + + common->pathname = NULL; + /* formatted_expr will be set later in transformJsonValueExpr(). */ + common->expr = makeJsonValueExpr((Expr *) contextItemExpr, NULL, + default_format); + common->passing = passingArgs; + + if (jtc->pathspec) + pathspec = jtc->pathspec; + else + { + /* Construct default path as '$."column_name"' */ + StringInfoData path; + + initStringInfo(&path); + + appendStringInfoString(&path, "$."); + escape_json(&path, jtc->name); + + pathspec = path.data; + } + + common->pathspec = makeStringConst(pathspec, -1); + + return (Node *) jfexpr; +} + +static bool +isJsonTablePathNameDuplicate(JsonTableParseContext *cxt, const char *pathname) +{ + ListCell *lc; + + foreach(lc, cxt->pathNames) + { + if (!strcmp(pathname, (const char *) lfirst(lc))) + return true; + } + + return false; +} + +/* Register the column name in the path name list. */ +static void +registerJsonTableColumn(JsonTableParseContext *cxt, char *colname) +{ + if (isJsonTablePathNameDuplicate(cxt, colname)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_ALIAS), + errmsg("duplicate JSON_TABLE column name: %s", colname), + errhint("JSON_TABLE column names must be distinct from one another."))); + + cxt->pathNames = lappend(cxt->pathNames, colname); +} + +/* Recursively register all nested column names in the path name list. */ +static void +registerAllJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED) + { + if (jtc->pathname) + registerJsonTableColumn(cxt, jtc->pathname); + + registerAllJsonTableColumns(cxt, jtc->columns); + } + else + { + registerJsonTableColumn(cxt, jtc->name); + } + } +} + +/* Generate a new unique JSON_TABLE path name. */ +static char * +generateJsonTablePathName(JsonTableParseContext *cxt) +{ + char namebuf[32]; + char *name = namebuf; + + do + { + snprintf(namebuf, sizeof(namebuf), "json_table_path_%d", + ++cxt->pathNameId); + } while (isJsonTablePathNameDuplicate(cxt, name)); + + name = pstrdup(name); + cxt->pathNames = lappend(cxt->pathNames, name); + + return name; +} + +/* Collect sibling path names from plan to the specified list. */ +static void +collectSiblingPathsInJsonTablePlan(JsonTablePlan *plan, List **paths) +{ + if (plan->plan_type == JSTP_SIMPLE) + *paths = lappend(*paths, plan->pathname); + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + *paths = lappend(*paths, plan->plan1->pathname); + } + else if (plan->join_type == JSTPJ_CROSS || + plan->join_type == JSTPJ_UNION) + { + collectSiblingPathsInJsonTablePlan(plan->plan1, paths); + collectSiblingPathsInJsonTablePlan(plan->plan2, paths); + } + else + elog(ERROR, "invalid JSON_TABLE join type %d", + plan->join_type); + } +} + +/* + * Validate child JSON_TABLE plan by checking that: + * - all nested columns have path names specified + * - all nested columns have corresponding node in the sibling plan + * - plan does not contain duplicate or extra nodes + */ +static void +validateJsonTableChildPlan(ParseState *pstate, JsonTablePlan *plan, + List *columns) +{ + ListCell *lc1; + List *siblings = NIL; + int nchildren = 0; + + if (plan) + collectSiblingPathsInJsonTablePlan(plan, &siblings); + + foreach(lc1, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc1)); + + if (jtc->coltype == JTC_NESTED) + { + ListCell *lc2; + bool found = false; + + if (!jtc->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("nested JSON_TABLE columns must contain an explicit AS pathname specification if an explicit PLAN clause is used"), + parser_errposition(pstate, jtc->location))); + + /* find nested path name in the list of sibling path names */ + foreach(lc2, siblings) + { + if ((found = !strcmp(jtc->pathname, lfirst(lc2)))) + break; + } + + if (!found) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node for nested path %s was not found in plan.", jtc->pathname), + parser_errposition(pstate, jtc->location))); + + nchildren++; + } + } + + if (list_length(siblings) > nchildren) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node contains some extra or duplicate sibling nodes."), + parser_errposition(pstate, plan ? plan->location : -1))); +} + +static JsonTableColumn * +findNestedJsonTableColumn(List *columns, const char *pathname) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED && + jtc->pathname && + !strcmp(jtc->pathname, pathname)) + return jtc; + } + + return NULL; +} + +static Node * +transformNestedJsonTableColumn(JsonTableParseContext *cxt, JsonTableColumn *jtc, + JsonTablePlan *plan) +{ + JsonTableParent *node; + char *pathname = jtc->pathname; + + node = transformJsonTableColumns(cxt, plan, jtc->columns, jtc->pathspec, + &pathname, jtc->location); + + return (Node *) node; +} + +static Node * +makeJsonTableSiblingJoin(bool cross, Node *lnode, Node *rnode) +{ + JsonTableSibling *join = makeNode(JsonTableSibling); + + join->larg = lnode; + join->rarg = rnode; + join->cross = cross; + + return (Node *) join; +} + +/* + * Recursively transform child JSON_TABLE plan. + * + * Default plan is transformed into a cross/union join of its nested columns. + * Simple and outer/inner plans are transformed into a JsonTableParent by + * finding and transforming corresponding nested column. + * Sibling plans are recursively transformed into a JsonTableSibling. + */ +static Node * +transformJsonTableChildPlan(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns) +{ + JsonTableColumn *jtc = NULL; + + if (!plan || plan->plan_type == JSTP_DEFAULT) + { + /* unspecified or default plan */ + Node *res = NULL; + ListCell *lc; + bool cross = plan && (plan->join_type & JSTPJ_CROSS); + + /* transform all nested columns into cross/union join */ + foreach(lc, columns) + { + JsonTableColumn *col = castNode(JsonTableColumn, lfirst(lc)); + Node *node; + + if (col->coltype != JTC_NESTED) + continue; + + node = transformNestedJsonTableColumn(cxt, col, plan); + + /* join transformed node with previous sibling nodes */ + res = res ? makeJsonTableSiblingJoin(cross, res, node) : node; + } + + return res; + } + else if (plan->plan_type == JSTP_SIMPLE) + { + jtc = findNestedJsonTableColumn(columns, plan->pathname); + } + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + jtc = findNestedJsonTableColumn(columns, plan->plan1->pathname); + } + else + { + Node *node1 = transformJsonTableChildPlan(cxt, plan->plan1, + columns); + Node *node2 = transformJsonTableChildPlan(cxt, plan->plan2, + columns); + + return makeJsonTableSiblingJoin(plan->join_type == JSTPJ_CROSS, + node1, node2); + } + } + else + elog(ERROR, "invalid JSON_TABLE plan type %d", plan->plan_type); + + if (!jtc) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name was %s not found in nested columns list.", + plan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + return transformNestedJsonTableColumn(cxt, jtc, plan); +} + +/* Check whether type is json/jsonb, array, or record. */ +static bool +typeIsComposite(Oid typid) +{ + char typtype; + + if (typid == JSONOID || + typid == JSONBOID || + typid == RECORDOID || + type_is_array(typid)) + return true; + + typtype = get_typtype(typid); + + if (typtype == TYPTYPE_COMPOSITE) + return true; + + if (typtype == TYPTYPE_DOMAIN) + return typeIsComposite(getBaseType(typid)); + + return false; +} + +/* Append transformed non-nested JSON_TABLE columns to the TableFunc node */ +static void +appendJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *col; + ParseState *pstate = cxt->pstate; + JsonTable *jt = cxt->table; + TableFunc *tf = cxt->tablefunc; + bool errorOnError = jt->on_error && + jt->on_error->btype == JSON_BEHAVIOR_ERROR; + + foreach(col, columns) + { + JsonTableColumn *rawc = castNode(JsonTableColumn, lfirst(col)); + Oid typid; + int32 typmod; + Node *colexpr; + + if (rawc->name) + { + /* make sure column names are unique */ + ListCell *colname; + + foreach(colname, tf->colnames) + if (!strcmp((const char *) colname, rawc->name)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("column name \"%s\" is not unique", + rawc->name), + parser_errposition(pstate, rawc->location))); + + tf->colnames = lappend(tf->colnames, + makeString(pstrdup(rawc->name))); + } + + /* + * Determine the type and typmod for the new column. FOR ORDINALITY + * columns are INTEGER by standard; the others are user-specified. + */ + switch (rawc->coltype) + { + case JTC_FOR_ORDINALITY: + colexpr = NULL; + typid = INT4OID; + typmod = -1; + break; + + case JTC_REGULAR: + typenameTypeIdAndMod(pstate, rawc->typeName, &typid, &typmod); + + /* + * Use implicit FORMAT JSON for composite types (arrays and + * records) + */ + if (typeIsComposite(typid)) + rawc->coltype = JTC_FORMATTED; + else if (rawc->wrapper != JSW_NONE) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use WITH WRAPPER clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + else if (rawc->omit_quotes) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use OMIT QUOTES clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + + /* FALLTHROUGH */ + case JTC_EXISTS: + case JTC_FORMATTED: + { + Node *je; + CaseTestExpr *param = makeNode(CaseTestExpr); + + param->collation = InvalidOid; + param->typeId = cxt->contextItemTypid; + param->typeMod = -1; + + je = transformJsonTableColumn(rawc, (Node *) param, + NIL, errorOnError); + + colexpr = transformExpr(pstate, je, EXPR_KIND_FROM_FUNCTION); + assign_expr_collations(pstate, colexpr); + + typid = exprType(colexpr); + typmod = exprTypmod(colexpr); + break; + } + + case JTC_NESTED: + continue; + + default: + elog(ERROR, "unknown JSON_TABLE column type: %d", rawc->coltype); + break; + } + + tf->coltypes = lappend_oid(tf->coltypes, typid); + tf->coltypmods = lappend_int(tf->coltypmods, typmod); + tf->colcollations = lappend_oid(tf->colcollations, get_typcollation(typid)); + tf->colvalexprs = lappend(tf->colvalexprs, colexpr); + } +} + +/* + * Create transformed JSON_TABLE parent plan node by appending all non-nested + * columns to the TableFunc node and remembering their indices in the + * colvalexprs list. + */ +static JsonTableParent * +makeParentJsonTableNode(JsonTableParseContext *cxt, char *pathSpec, char *pathName, + List *columns) +{ + JsonTableParent *node = makeNode(JsonTableParent); + + node->path = makeNode(JsonTablePath); + node->path->value = makeConst(JSONPATHOID, -1, InvalidOid, -1, + DirectFunctionCall1(jsonpath_in, + CStringGetDatum(pathSpec)), + false, false); + if (pathName) + node->path->name = pstrdup(pathName); + + /* save start of column range */ + node->colMin = list_length(cxt->tablefunc->colvalexprs); + + appendJsonTableColumns(cxt, columns); + + /* save end of column range */ + node->colMax = list_length(cxt->tablefunc->colvalexprs) - 1; + + node->errorOnError = + cxt->table->on_error && + cxt->table->on_error->btype == JSON_BEHAVIOR_ERROR; + + return node; +} + +static JsonTableParent * +transformJsonTableColumns(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns, char *pathSpec, char **pathName, + int location) +{ + JsonTableParent *node; + JsonTablePlan *childPlan; + bool defaultPlan = !plan || plan->plan_type == JSTP_DEFAULT; + + if (!*pathName) + { + if (cxt->table->plan) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE expression"), + errdetail("JSON_TABLE columns must contain " + "explicit AS pathname specification if " + "explicit PLAN clause is used"), + parser_errposition(cxt->pstate, location))); + + *pathName = generateJsonTablePathName(cxt); + } + + if (defaultPlan) + childPlan = plan; + else + { + /* validate parent and child plans */ + JsonTablePlan *parentPlan; + + if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type != JSTPJ_INNER && + plan->join_type != JSTPJ_OUTER) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Expected INNER or OUTER JSON_TABLE plan node."), + parser_errposition(cxt->pstate, plan->location))); + + parentPlan = plan->plan1; + childPlan = plan->plan2; + + Assert(parentPlan->plan_type != JSTP_JOINED); + Assert(parentPlan->pathname); + } + else + { + parentPlan = plan; + childPlan = NULL; + } + + if (strcmp(parentPlan->pathname, *pathName)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name mismatch: expected %s but %s is given.", + *pathName, parentPlan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + validateJsonTableChildPlan(cxt->pstate, childPlan, columns); + } + + /* transform only non-nested columns */ + node = makeParentJsonTableNode(cxt, pathSpec, *pathName, columns); + + if (childPlan || defaultPlan) + { + /* transform recursively nested columns */ + node->child = transformJsonTableChildPlan(cxt, childPlan, columns); + if (node->child) + node->outerJoin = !plan || (plan->join_type & JSTPJ_OUTER); + /* else: default plan case, no children found */ + } + + return node; +} + +/* + * transformJsonTable - + * Transform a raw JsonTable into TableFunc. + * + * Transform the document-generating expression, the row-generating expression, + * the column-generating expressions, and the default value expressions. + */ +ParseNamespaceItem * +transformJsonTable(ParseState *pstate, JsonTable *jt) +{ + JsonTableParseContext cxt; + TableFunc *tf = makeNode(TableFunc); + JsonFuncExpr *jfe = makeNode(JsonFuncExpr); + JsonExpr *je; + JsonTablePlan *plan = jt->plan; + JsonCommon *jscommon; + char *rootPathName = jt->common->pathname; + char *rootPath; + bool is_lateral; + + cxt.pstate = pstate; + cxt.table = jt; + cxt.tablefunc = tf; + cxt.pathNames = NIL; + cxt.pathNameId = 0; + + if (rootPathName) + registerJsonTableColumn(&cxt, rootPathName); + + registerAllJsonTableColumns(&cxt, jt->columns); + +#if 0 /* XXX it' unclear from the standard whether + * root path name is mandatory or not */ + if (plan && plan->plan_type != JSTP_DEFAULT && !rootPathName) + { + /* Assign root path name and create corresponding plan node */ + JsonTablePlan *rootNode = makeNode(JsonTablePlan); + JsonTablePlan *rootPlan = (JsonTablePlan *) + makeJsonTableJoinedPlan(JSTPJ_OUTER, (Node *) rootNode, + (Node *) plan, jt->location); + + rootPathName = generateJsonTablePathName(&cxt); + + rootNode->plan_type = JSTP_SIMPLE; + rootNode->pathname = rootPathName; + + plan = rootPlan; + } +#endif + + jscommon = copyObject(jt->common); + jscommon->pathspec = makeStringConst(pstrdup("$"), -1); + + jfe->op = JSON_TABLE_OP; + jfe->common = jscommon; + jfe->on_error = jt->on_error; + jfe->location = jt->common->location; + + /* + * We make lateral_only names of this level visible, whether or not the + * RangeTableFunc is explicitly marked LATERAL. This is needed for SQL + * spec compliance and seems useful on convenience grounds for all + * functions in FROM. + * + * (LATERAL can't nest within a single pstate level, so we don't need + * save/restore logic here.) + */ + Assert(!pstate->p_lateral_active); + pstate->p_lateral_active = true; + + tf->functype = TFT_JSON_TABLE; + tf->docexpr = transformExpr(pstate, (Node *) jfe, EXPR_KIND_FROM_FUNCTION); + + cxt.contextItemTypid = exprType(tf->docexpr); + + if (!IsA(jt->common->pathspec, A_Const) || + castNode(A_Const, jt->common->pathspec)->val.node.type != T_String) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("only string constants supported in JSON_TABLE path specification"), + parser_errposition(pstate, + exprLocation(jt->common->pathspec)))); + + rootPath = castNode(A_Const, jt->common->pathspec)->val.sval.sval; + + tf->plan = (Node *) transformJsonTableColumns(&cxt, plan, jt->columns, + rootPath, &rootPathName, + jt->common->location); + + /* Also save a copy of the PASSING arguments in the TableFunc node. */ + je = (JsonExpr *) tf->docexpr; + tf->passingvalexprs = copyObject(je->passing_values); + + tf->ordinalitycol = -1; /* undefine ordinality column number */ + tf->location = jt->location; + + pstate->p_lateral_active = false; + + /* + * Mark the RTE as LATERAL if the user said LATERAL explicitly, or if + * there are any lateral cross-references in it. + */ + is_lateral = jt->lateral || contain_vars_of_level((Node *) tf, 0); + + return addRangeTableEntryForTableFunc(pstate, + tf, jt->alias, is_lateral, true); +} diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 864ea9b0d5..3ac7e113f0 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -2073,7 +2073,8 @@ addRangeTableEntryForTableFunc(ParseState *pstate, Assert(list_length(tf->coltypmods) == list_length(tf->colnames)); Assert(list_length(tf->colcollations) == list_length(tf->colnames)); - refname = alias ? alias->aliasname : pstrdup("xmltable"); + refname = alias ? alias->aliasname : + pstrdup(tf->functype == TFT_XMLTABLE ? "xmltable" : "json_table"); rte->rtekind = RTE_TABLEFUNC; rte->relid = InvalidOid; @@ -2096,7 +2097,7 @@ addRangeTableEntryForTableFunc(ParseState *pstate, ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), errmsg("%s function has %d columns available but %d columns specified", - "XMLTABLE", + tf->functype == TFT_XMLTABLE ? "XMLTABLE" : "JSON_TABLE", list_length(tf->colnames), numaliases))); rte->eref = eref; diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 2c83d3a3a9..5bfd79af48 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1995,6 +1995,9 @@ FigureColnameInternal(Node *node, char **name) case JSON_EXISTS_OP: *name = "json_exists"; return 2; + case JSON_TABLE_OP: + *name = "json_table"; + return 2; } break; default: diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index eded22e194..92d76d5cde 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -61,10 +61,12 @@ #include "catalog/pg_collation.h" #include "catalog/pg_type.h" +#include "executor/execExpr.h" #include "funcapi.h" #include "lib/stringinfo.h" #include "miscadmin.h" #include "nodes/miscnodes.h" +#include "nodes/nodeFuncs.h" #include "regex/regex.h" #include "utils/builtins.h" #include "utils/date.h" @@ -75,6 +77,8 @@ #include "utils/guc.h" #include "utils/json.h" #include "utils/jsonpath.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" #include "utils/timestamp.h" #include "utils/varlena.h" @@ -156,6 +160,61 @@ typedef struct JsonValueListIterator ListCell *next; } JsonValueListIterator; +/* Structures for JSON_TABLE execution */ + +typedef enum JsonTablePlanStateType +{ + JSON_TABLE_SCAN_STATE = 0, + JSON_TABLE_JOIN_STATE +} JsonTablePlanStateType; + +typedef struct JsonTablePlanState +{ + JsonTablePlanStateType type; + + struct JsonTablePlanState *parent; + struct JsonTablePlanState *nested; +} JsonTablePlanState; + +typedef struct JsonTableScanState +{ + JsonTablePlanState plan; + + MemoryContext mcxt; + JsonPath *path; + List *args; + JsonValueList found; + JsonValueListIterator iter; + Datum current; + int ordinal; + bool currentIsNull; + bool outerJoin; + bool errorOnError; + bool advanceNested; + bool reset; +} JsonTableScanState; + +typedef struct JsonTableJoinState +{ + JsonTablePlanState plan; + + JsonTablePlanState *left; + JsonTablePlanState *right; + bool cross; + bool advanceRight; +} JsonTableJoinState; + +/* random number to identify JsonTableExecContext */ +#define JSON_TABLE_EXEC_CONTEXT_MAGIC 418352867 + +typedef struct JsonTableExecContext +{ + int magic; + JsonTableScanState **colexprscans; + JsonTableScanState *root; + bool empty; +} JsonTableExecContext; + /* strict/lax flags is decomposed into four [un]wrap/error flags */ #define jspStrictAbsenseOfErrors(cxt) (!(cxt)->laxMode) #define jspAutoUnwrap(cxt) ((cxt)->laxMode) @@ -248,6 +307,7 @@ static JsonPathExecResult getArrayIndex(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbValue *jb, int32 *index); static JsonBaseObjectInfo setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id); +static void JsonValueListClear(JsonValueList *jvl); static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv); static int JsonValueListLength(const JsonValueList *jvl); static bool JsonValueListIsEmpty(JsonValueList *jvl); @@ -265,6 +325,14 @@ static JsonbValue *wrapItemsInArray(const JsonValueList *items); static int compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, bool useTz, bool *cast_error); + +static JsonTablePlanState *JsonTableInitPlanState(JsonTableExecContext *cxt, + Node *plan, + JsonTablePlanState *parent); +static bool JsonTablePlanNextRow(JsonTablePlanState *state); +static bool JsonTableScanNextRow(JsonTableScanState *scan); + + /****************** User interface to JsonPath executor ********************/ /* @@ -2518,6 +2586,13 @@ setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id) return baseObject; } +static void +JsonValueListClear(JsonValueList *jvl) +{ + jvl->singleton = NULL; + jvl->list = NULL; +} + static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv) { @@ -3123,3 +3198,471 @@ JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); } } + +/************************ JSON_TABLE functions ***************************/ + +/* + * Returns private data from executor state. Ensure validity by check with + * MAGIC number. + */ +static inline JsonTableExecContext * +GetJsonTableExecContext(TableFuncScanState *state, const char *fname) +{ + JsonTableExecContext *result; + + if (!IsA(state, TableFuncScanState)) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + result = (JsonTableExecContext *) state->opaque; + if (result->magic != JSON_TABLE_EXEC_CONTEXT_MAGIC) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + + return result; +} + +/* Recursively initialize JSON_TABLE scan / join state */ +static JsonTableJoinState * +JsonTableInitJoinState(JsonTableExecContext *cxt, JsonTableSibling *plan, + JsonTablePlanState *parent) +{ + JsonTableJoinState *join = palloc0(sizeof(*join)); + + join->plan.type = JSON_TABLE_JOIN_STATE; + /* parent and nested not set. */ + + join->cross = plan->cross; + join->left = JsonTableInitPlanState(cxt, plan->larg, parent); + join->right = JsonTableInitPlanState(cxt, plan->rarg, parent); + + return join; +} + +static JsonTableScanState * +JsonTableInitScanState(JsonTableExecContext *cxt, JsonTableParent *plan, + JsonTablePlanState *parent, + List *args, MemoryContext mcxt) +{ + JsonTableScanState *scan = palloc0(sizeof(*scan)); + int i; + + scan->plan.type = JSON_TABLE_SCAN_STATE; + scan->plan.parent = parent; + + scan->outerJoin = plan->outerJoin; + scan->errorOnError = plan->errorOnError; + scan->path = DatumGetJsonPathP(plan->path->value->constvalue); + scan->args = args; + scan->mcxt = AllocSetContextCreate(mcxt, "JsonTableExecContext", + ALLOCSET_DEFAULT_SIZES); + + /* + * Set after settings scan->args and scan->mcxt, because the recursive call + * wants to use those values. + */ + scan->plan.nested = plan->child ? + JsonTableInitPlanState(cxt, plan->child, (JsonTablePlanState *) scan) : + NULL; + + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + + for (i = plan->colMin; i <= plan->colMax; i++) + cxt->colexprscans[i] = scan; + + return scan; +} + +/* Recursively initialize JSON_TABLE scan state */ +static JsonTablePlanState * +JsonTableInitPlanState(JsonTableExecContext *cxt, Node *plan, + JsonTablePlanState *parent) +{ + JsonTablePlanState *state; + + if (IsA(plan, JsonTableSibling)) + { + JsonTableSibling *join = castNode(JsonTableSibling, plan); + + state = (JsonTablePlanState *) + JsonTableInitJoinState(cxt, join, parent); + } + else + { + JsonTableParent *scan = castNode(JsonTableParent, plan); + JsonTableScanState *parent_scan = (JsonTableScanState *) parent; + + Assert(parent_scan); + state = (JsonTablePlanState *) + JsonTableInitScanState(cxt, scan, parent, parent_scan->args, + parent_scan->mcxt); + } + + return state; +} + +/* + * JsonTableInitOpaque + * Fill in TableFuncScanState->opaque for JsonTable processor + */ +static void +JsonTableInitOpaque(TableFuncScanState *state, int natts) +{ + JsonTableExecContext *cxt; + PlanState *ps = &state->ss.ps; + TableFuncScan *tfs = castNode(TableFuncScan, ps->plan); + TableFunc *tf = tfs->tablefunc; + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + JsonExpr *je = castNode(JsonExpr, tf->docexpr); + List *args = NIL; + + cxt = palloc0(sizeof(JsonTableExecContext)); + cxt->magic = JSON_TABLE_EXEC_CONTEXT_MAGIC; + + if (state->passingvalexprs) + { + ListCell *exprlc; + ListCell *namelc; + + Assert(list_length(state->passingvalexprs) == + list_length(je->passing_names)); + forboth(exprlc, state->passingvalexprs, + namelc, je->passing_names) + { + ExprState *state = lfirst_node(ExprState, exprlc); + String *name = lfirst_node(String, namelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(name->sval); + var->typid = exprType((Node *) state->expr); + var->typmod = exprTypmod((Node *) state->expr); + + /* + * Evaluate the expression and save the value to be returned by + * GetJsonPathVar(). + */ + var->value = ExecEvalExpr(state, ps->ps_ExprContext, + &var->isnull); + + args = lappend(args, var); + } + } + + cxt->colexprscans = palloc(sizeof(JsonTableScanState *) * + list_length(tf->colvalexprs)); + + cxt->root = JsonTableInitScanState(cxt, root, NULL, args, + CurrentMemoryContext); + state->opaque = cxt; +} + +/* Reset scan iterator to the beginning of the item list */ +static void +JsonTableRescan(JsonTableScanState *scan) +{ + JsonValueListInitIterator(&scan->found, &scan->iter); + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + scan->advanceNested = false; + scan->ordinal = 0; +} + +/* Reset context item of a scan, execute JSON path and reset a scan */ +static void +JsonTableResetContextItem(JsonTableScanState *scan, Datum item) +{ + MemoryContext oldcxt; + JsonPathExecResult res; + Jsonb *js = (Jsonb *) DatumGetJsonbP(item); + + JsonValueListClear(&scan->found); + + MemoryContextResetOnly(scan->mcxt); + + oldcxt = MemoryContextSwitchTo(scan->mcxt); + + res = executeJsonPath(scan->path, scan->args, GetJsonPathVar, js, + scan->errorOnError, &scan->found, + false /* FIXME */ ); + + MemoryContextSwitchTo(oldcxt); + + if (jperIsError(res)) + { + Assert(!scan->errorOnError); + JsonValueListClear(&scan->found); /* EMPTY ON ERROR case */ + } + + JsonTableRescan(scan); +} + +/* + * JsonTableSetDocument + * Install the input document + */ +static void +JsonTableSetDocument(TableFuncScanState *state, Datum value) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableSetDocument"); + + JsonTableResetContextItem(cxt->root, value); +} + +/* Recursively reset scan and its child nodes */ +static void +JsonTableRescanRecursive(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTableRescanRecursive(join->left); + JsonTableRescanRecursive(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan = (JsonTableScanState *) state; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + JsonTableRescan(scan); + if (scan->plan.nested) + JsonTableRescanRecursive(scan->plan.nested); + } +} + +/* + * Fetch next row from a cross/union joined scan. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTablePlanNextRow(JsonTablePlanState *state) +{ + JsonTableJoinState *join; + + if (state->type == JSON_TABLE_SCAN_STATE) + return JsonTableScanNextRow((JsonTableScanState *) state); + + join = (JsonTableJoinState *) state; + if (join->advanceRight) + { + /* fetch next inner row */ + if (JsonTablePlanNextRow(join->right)) + return true; + + /* inner rows are exhausted */ + if (join->cross) + join->advanceRight = false; /* next outer row */ + else + return false; /* end of scan */ + } + + while (!join->advanceRight) + { + /* fetch next outer row */ + bool left = JsonTablePlanNextRow(join->left); + + if (join->cross) + { + if (!left) + return false; /* end of scan */ + + JsonTableRescanRecursive(join->right); + + if (!JsonTablePlanNextRow(join->right)) + continue; /* next outer row */ + + join->advanceRight = true; /* next inner row */ + } + else if (!left) + { + if (!JsonTablePlanNextRow(join->right)) + return false; /* end of scan */ + + join->advanceRight = true; /* next inner row */ + } + + break; + } + + return true; +} + +/* Recursively set 'reset' flag of scan and its child nodes */ +static void +JsonTablePlanReset(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTablePlanReset(join->left); + JsonTablePlanReset(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + scan = (JsonTableScanState *) state; + scan->reset = true; + scan->advanceNested = false; + + if (scan->plan.nested) + JsonTablePlanReset(scan->plan.nested); + } +} + +/* + * Fetch next row from a simple scan with outer/inner joined nested subscans. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTableScanNextRow(JsonTableScanState *scan) +{ + /* reset context item if requested */ + if (scan->reset) + { + JsonTableScanState *parent_scan = + (JsonTableScanState *) scan->plan.parent; + + Assert(parent_scan && !parent_scan->currentIsNull); + JsonTableResetContextItem(scan, parent_scan->current); + scan->reset = false; + } + + if (scan->advanceNested) + { + /* fetch next nested row */ + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested) + return true; + } + + for (;;) + { + /* fetch next row */ + JsonbValue *jbv = JsonValueListNext(&scan->found, &scan->iter); + MemoryContext oldcxt; + + if (!jbv) + { + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + return false; /* end of scan */ + } + + /* set current row item */ + oldcxt = MemoryContextSwitchTo(scan->mcxt); + scan->current = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + scan->currentIsNull = false; + MemoryContextSwitchTo(oldcxt); + + scan->ordinal++; + + if (!scan->plan.nested) + break; + + JsonTablePlanReset(scan->plan.nested); + + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested || scan->outerJoin) + break; + } + + return true; +} + +/* + * JsonTableFetchRow + * Prepare the next "current" tuple for upcoming GetValue calls. + * Returns FALSE if the row-filter expression returned no more rows. + */ +static bool +JsonTableFetchRow(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableFetchRow"); + + if (cxt->empty) + return false; + + return JsonTableScanNextRow(cxt->root); +} + +/* + * JsonTableGetValue + * Return the value for column number 'colnum' for the current row. + * + * This leaks memory, so be sure to reset often the context in which it's + * called. + */ +static Datum +JsonTableGetValue(TableFuncScanState *state, int colnum, + Oid typid, int32 typmod, bool *isnull) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableGetValue"); + ExprContext *econtext = state->ss.ps.ps_ExprContext; + ExprState *estate = list_nth(state->colvalexprs, colnum); + JsonTableScanState *scan = cxt->colexprscans[colnum]; + Datum result; + + if (scan->currentIsNull) /* NULL from outer/union join */ + { + result = (Datum) 0; + *isnull = true; + } + else if (estate) /* regular column */ + { + Datum saved_caseValue = econtext->caseValue_datum; + bool saved_caseIsNull = econtext->caseValue_isNull; + + /* Pass the value for CaseTestExpr that may be present in colexpr */ + econtext->caseValue_datum = scan->current; + econtext->caseValue_isNull = false; + + result = ExecEvalExpr(estate, econtext, isnull); + + econtext->caseValue_datum = saved_caseValue; + econtext->caseValue_isNull = saved_caseIsNull; + } + else + { + result = Int32GetDatum(scan->ordinal); /* ordinality column */ + *isnull = false; + } + + return result; +} + +/* + * JsonTableDestroyOpaque + */ +static void +JsonTableDestroyOpaque(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableDestroyOpaque"); + + /* not valid anymore */ + cxt->magic = 0; + + state->opaque = NULL; +} + +const TableFuncRoutine JsonbTableRoutine = +{ + JsonTableInitOpaque, + JsonTableSetDocument, + NULL, + NULL, + NULL, + JsonTableFetchRow, + JsonTableGetValue, + JsonTableDestroyOpaque +}; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d4ebeb18dd..0c1cecd24e 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -522,6 +522,8 @@ static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); static void get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit); +static void get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8606,7 +8608,8 @@ get_json_behavior(JsonBehavior *behavior, deparse_context *context, /* * get_json_expr_options * - * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS and + * JSON_TABLE columns. */ static void get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, @@ -9854,6 +9857,9 @@ get_rule_expr(Node *node, deparse_context *context, case JSON_EXISTS_OP: appendStringInfoString(buf, "JSON_EXISTS("); break; + default: + elog(ERROR, "unexpected JsonExpr type: %d", jexpr->op); + break; } get_rule_expr(jexpr->formatted_expr, context, showimplicit); @@ -11209,16 +11215,14 @@ get_sublink_expr(SubLink *sublink, deparse_context *context) /* ---------- - * get_tablefunc - Parse back a table function + * get_xmltable - Parse back a XMLTABLE function * ---------- */ static void -get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +get_xmltable(TableFunc *tf, deparse_context *context, bool showimplicit) { StringInfo buf = context->buf; - /* XMLTABLE is the only existing implementation. */ - appendStringInfoString(buf, "XMLTABLE("); if (tf->ns_uris != NIL) @@ -11309,6 +11313,271 @@ get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) appendStringInfoChar(buf, ')'); } +/* + * get_json_nested_columns - Parse back nested JSON_TABLE columns + */ +static void +get_json_table_nested_columns(TableFunc *tf, Node *node, + deparse_context *context, bool showimplicit, + bool needcomma) +{ + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_nested_columns(tf, n->larg, context, showimplicit, + needcomma); + get_json_table_nested_columns(tf, n->rarg, context, showimplicit, true); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + if (needcomma) + appendStringInfoChar(context->buf, ','); + + appendStringInfoChar(context->buf, ' '); + appendContextKeyword(context, "NESTED PATH ", 0, 0, 0); + get_const_expr(n->path->value, context, -1); + appendStringInfo(context->buf, " AS %s", quote_identifier(n->path->name)); + get_json_table_columns(tf, n, context, showimplicit); + } +} + +/* + * get_json_table_plan - Parse back a JSON_TABLE plan + */ +static void +get_json_table_plan(TableFunc *tf, Node *node, deparse_context *context, + bool parenthesize) +{ + if (parenthesize) + appendStringInfoChar(context->buf, '('); + + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_plan(tf, n->larg, context, + IsA(n->larg, JsonTableSibling) || + castNode(JsonTableParent, n->larg)->child); + + appendStringInfoString(context->buf, n->cross ? " CROSS " : " UNION "); + + get_json_table_plan(tf, n->rarg, context, + IsA(n->rarg, JsonTableSibling) || + castNode(JsonTableParent, n->rarg)->child); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + appendStringInfoString(context->buf, quote_identifier(n->path->name)); + + if (n->child) + { + appendStringInfoString(context->buf, + n->outerJoin ? " OUTER " : " INNER "); + get_json_table_plan(tf, n->child, context, + IsA(n->child, JsonTableSibling)); + } + } + + if (parenthesize) + appendStringInfoChar(context->buf, ')'); +} + +/* + * get_json_table_columns - Parse back JSON_TABLE columns + */ +static void +get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + ListCell *lc_colname; + ListCell *lc_coltype; + ListCell *lc_coltypmod; + ListCell *lc_colvalexpr; + int colnum = 0; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "COLUMNS (", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forfour(lc_colname, tf->colnames, + lc_coltype, tf->coltypes, + lc_coltypmod, tf->coltypmods, + lc_colvalexpr, tf->colvalexprs) + { + char *colname = strVal(lfirst(lc_colname)); + JsonExpr *colexpr; + Oid typid; + int32 typmod; + bool ordinality; + JsonBehaviorType default_behavior; + + typid = lfirst_oid(lc_coltype); + typmod = lfirst_int(lc_coltypmod); + colexpr = castNode(JsonExpr, lfirst(lc_colvalexpr)); + + if (colnum < node->colMin) + { + colnum++; + continue; + } + + if (colnum > node->colMax) + break; + + if (colnum > node->colMin) + appendStringInfoString(buf, ", "); + + colnum++; + + ordinality = !colexpr; + + appendContextKeyword(context, "", 0, 0, 0); + + appendStringInfo(buf, "%s %s", quote_identifier(colname), + ordinality ? "FOR ORDINALITY" : + format_type_with_typemod(typid, typmod)); + if (ordinality) + continue; + + if (colexpr->op == JSON_EXISTS_OP) + { + appendStringInfoString(buf, " EXISTS"); + default_behavior = JSON_BEHAVIOR_FALSE; + } + else + { + if (colexpr->op == JSON_QUERY_OP) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(typid, &typcategory, &typispreferred); + + if (typcategory == TYPCATEGORY_STRING) + appendStringInfoString(buf, + colexpr->format->format_type == JS_FORMAT_JSONB ? + " FORMAT JSONB" : " FORMAT JSON"); + } + + default_behavior = JSON_BEHAVIOR_NULL; + } + + if (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR) + default_behavior = JSON_BEHAVIOR_ERROR; + + appendStringInfoString(buf, " PATH "); + + get_json_path_spec(colexpr->path_spec, context, showimplicit); + + get_json_expr_options(colexpr, context, default_behavior); + } + + if (node->child) + get_json_table_nested_columns(tf, node->child, context, showimplicit, + node->colMax >= node->colMin); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_json_table - Parse back a JSON_TABLE function + * ---------- + */ +static void +get_json_table(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + + appendStringInfoString(buf, "JSON_TABLE("); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_const_expr(root->path->value, context, -1); + + appendStringInfo(buf, " AS %s", quote_identifier(root->path->name)); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PASSING ", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr((Node *) lfirst(lc2), context, false); + appendStringInfo(buf, " AS %s", + quote_identifier((lfirst_node(String, lc1))->sval) + ); + } + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + } + + get_json_table_columns(tf, root, context, showimplicit); + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PLAN ", 0, 0, 0); + get_json_table_plan(tf, (Node *) root, context, true); + + if (jexpr->on_error->btype != JSON_BEHAVIOR_EMPTY) + get_json_behavior(jexpr->on_error, context, "ERROR"); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_tablefunc - Parse back a table function + * ---------- + */ +static void +get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + /* XMLTABLE and JSON_TABLE are the only existing implementations. */ + + if (tf->functype == TFT_XMLTABLE) + get_xmltable(tf, context, showimplicit); + else if (tf->functype == TFT_JSON_TABLE) + get_json_table(tf, context, showimplicit); +} + /* ---------- * get_from_clause - Parse back a FROM clause * diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 584bf7001a..91453681e3 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1879,6 +1879,8 @@ typedef struct TableFuncScanState ExprState *rowexpr; /* state for row-generating expression */ List *colexprs; /* state for column-generating expression */ List *coldefexprs; /* state for column default expressions */ + List *colvalexprs; /* state for column value expression */ + List *passingvalexprs; /* state for PASSING argument expression */ List *ns_names; /* same as TableFunc.ns_names */ List *ns_uris; /* list of states of namespace URI exprs */ Bitmapset *notnulls; /* nullability flag for each output column */ diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index c1d437b44e..076b1ddab3 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -113,6 +113,8 @@ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, extern JsonValueExpr *makeJsonValueExpr(Expr *raw_expr, Expr *formatted_expr, JsonFormat *format); extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); +extern Node *makeJsonTableJoinedPlan(JsonTablePlanJoinType type, + Node *plan1, Node *plan2, int location); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 7d63a37d5f..b15f71cc92 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1733,6 +1733,19 @@ typedef enum JsonQuotes */ typedef char *JsonPathSpec; +/* + * JsonTableColumnType - + * enumeration of JSON_TABLE column types + */ +typedef enum JsonTableColumnType +{ + JTC_FOR_ORDINALITY, + JTC_REGULAR, + JTC_EXISTS, + JTC_FORMATTED, + JTC_NESTED, +} JsonTableColumnType; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1786,6 +1799,83 @@ typedef struct JsonFuncExpr int location; /* token location, or -1 if unknown */ } JsonFuncExpr; +/* + * JsonTableColumn - + * untransformed representation of JSON_TABLE column + */ +typedef struct JsonTableColumn +{ + NodeTag type; + JsonTableColumnType coltype; /* column type */ + char *name; /* column name */ + TypeName *typeName; /* column type name */ + char *pathspec; /* path specification, if any */ + char *pathname; /* path name, if any */ + JsonFormat *format; /* JSON format clause, if specified */ + JsonWrapper wrapper; /* WRAPPER behavior for formatted columns */ + bool omit_quotes; /* omit or keep quotes on scalar strings? */ + List *columns; /* nested columns */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + int location; /* token location, or -1 if unknown */ +} JsonTableColumn; + +/* + * JsonTablePlanType - + * flags for JSON_TABLE plan node types representation + */ +typedef enum JsonTablePlanType +{ + JSTP_DEFAULT, + JSTP_SIMPLE, + JSTP_JOINED, +} JsonTablePlanType; + +/* + * JsonTablePlanJoinType - + * flags for JSON_TABLE join types representation + */ +typedef enum JsonTablePlanJoinType +{ + JSTPJ_INNER = 0x01, + JSTPJ_OUTER = 0x02, + JSTPJ_CROSS = 0x04, + JSTPJ_UNION = 0x08, +} JsonTablePlanJoinType; + +typedef struct JsonTablePlan JsonTablePlan; + +/* + * JsonTablePlan - + * untransformed representation of JSON_TABLE plan node + */ +struct JsonTablePlan +{ + NodeTag type; + JsonTablePlanType plan_type; /* plan type */ + JsonTablePlanJoinType join_type; /* join type (for joined plan only) */ + JsonTablePlan *plan1; /* first joined plan */ + JsonTablePlan *plan2; /* second joined plan */ + char *pathname; /* path name (for simple plan only) */ + int location; /* token location, or -1 if unknown */ +}; + +/* + * JsonTable - + * untransformed representation of JSON_TABLE + */ +typedef struct JsonTable +{ + NodeTag type; + JsonCommon *common; /* common JSON path syntax fields */ + List *columns; /* list of JsonTableColumn */ + JsonTablePlan *plan; /* join plan, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + Alias *alias; /* table alias in FROM clause */ + bool lateral; /* does it have LATERAL prefix? */ + int location; /* token location, or -1 if unknown */ +} JsonTable; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 64fdbc57fa..bae1de3554 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -86,8 +86,14 @@ typedef struct RangeVar int location; } RangeVar; +typedef enum TableFuncType +{ + TFT_XMLTABLE, + TFT_JSON_TABLE +} TableFuncType; + /* - * TableFunc - node for a table function, such as XMLTABLE. + * TableFunc - node for a table function, such as XMLTABLE or JSON_TABLE. * * Entries in the ns_names list are either String nodes containing * literal namespace names, or NULL pointers to represent DEFAULT. @@ -95,6 +101,8 @@ typedef struct RangeVar typedef struct TableFunc { NodeTag type; + /* XMLTABLE or JSON_TABLE */ + TableFuncType functype; /* list of namespace URI expressions */ List *ns_uris pg_node_attr(query_jumble_ignore); /* list of namespace names or NULL */ @@ -115,8 +123,14 @@ typedef struct TableFunc List *colexprs; /* list of column default expressions */ List *coldefexprs pg_node_attr(query_jumble_ignore); + /* list of column value expressions */ + List *colvalexprs pg_node_attr(query_jumble_ignore); + /* list of PASSING argument expressions */ + List *passingvalexprs pg_node_attr(query_jumble_ignore); /* nullability flag for each output column */ Bitmapset *notnulls pg_node_attr(query_jumble_ignore); + /* JSON_TABLE plan */ + Node *plan pg_node_attr(query_jumble_ignore); /* counts from 0; -1 if none specified */ int ordinalitycol pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ @@ -1552,7 +1566,8 @@ typedef enum JsonExprOp { JSON_VALUE_OP, /* JSON_VALUE() */ JSON_QUERY_OP, /* JSON_QUERY() */ - JSON_EXISTS_OP /* JSON_EXISTS() */ + JSON_EXISTS_OP, /* JSON_EXISTS() */ + JSON_TABLE_OP /* JSON_TABLE() */ } JsonExprOp; /* @@ -1771,6 +1786,48 @@ typedef struct JsonExpr int location; /* token location, or -1 if unknown */ } JsonExpr; +/* + * JsonTablePath + * A JSON path expression to be computed as part of evaluating + * a JSON_TABLE plan node + */ +typedef struct JsonTablePath +{ + NodeTag type; + + Const *value; + char *name; +} JsonTablePath; + +/* + * JsonTableParent - + * transformed representation of parent JSON_TABLE plan node + */ +typedef struct JsonTableParent +{ + NodeTag type; + JsonTablePath *path; + Node *child; /* nested columns, if any */ + bool outerJoin; /* outer or inner join for nested columns? */ + int colMin; /* min column index in the resulting column + * list */ + int colMax; /* max column index in the resulting column + * list */ + bool errorOnError; /* ERROR/EMPTY ON ERROR behavior */ +} JsonTableParent; + +/* + * JsonTableSibling - + * transformed representation of joined sibling JSON_TABLE plan node + */ +typedef struct JsonTableSibling +{ + NodeTag type; + Node *larg; /* left join node */ + Node *rarg; /* right join node */ + bool cross; /* cross or union join? */ +} JsonTableSibling; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0954d9fc7b..f88a6c9ac6 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -242,6 +242,7 @@ PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_table", JSON_TABLE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) @@ -284,6 +285,7 @@ PG_KEYWORD("names", NAMES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("national", NATIONAL, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("natural", NATURAL, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("nchar", NCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("nested", NESTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("new", NEW, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("next", NEXT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("nfc", NFC, UNRESERVED_KEYWORD, BARE_LABEL) @@ -334,7 +336,9 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h index 8e26482db4..7152a8ef48 100644 --- a/src/include/parser/parse_clause.h +++ b/src/include/parser/parse_clause.h @@ -51,4 +51,7 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle, extern Index assignSortGroupRef(TargetEntry *tle, List *tlist); extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList); +/* functions in parse_jsontable.c */ +extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt); + #endif /* PARSE_CLAUSE_H */ diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index 4dae78a98c..b7ada77cb1 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -15,6 +15,7 @@ #define JSONPATH_H #include "fmgr.h" +#include "executor/tablefunc.h" #include "nodes/pg_list.h" #include "nodes/primnodes.h" #include "utils/jsonb.h" @@ -288,4 +289,6 @@ extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars); +extern PGDLLIMPORT const TableFuncRoutine JsonbTableRoutine; + #endif diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out index 8b87580752..995f267404 100644 --- a/src/test/regress/expected/json_sqljson.out +++ b/src/test/regress/expected/json_sqljson.out @@ -16,3 +16,9 @@ ERROR: JSON_QUERY() is not yet implemented for the json type LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); ^ HINT: Try casting the argument to jsonb +-- JSON_TABLE +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); +ERROR: JSON_TABLE() is not yet implemented for the json type +LINE 1: SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo ... + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out index 22f8679917..4323ed6b35 100644 --- a/src/test/regress/expected/jsonb_sqljson.out +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -1040,3 +1040,1072 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.dateti ERROR: functions in index expression must be marked IMMUTABLE CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; +-- JSON_TABLE +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); +ERROR: syntax error at or near "(" +LINE 1: SELECT JSON_TABLE('[]', '$'); + ^ +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); +ERROR: syntax error at or near ")" +LINE 1: SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + ^ +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); +ERROR: JSON_TABLE function has 1 columns available but 2 columns specified +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + foo +----- +(0 rows) + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + item | foo +------+----- + 123 | +(1 row) + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + js | id | id2 | int | text | char(4) | bool | numeric | domain | js | jb | jst | jsc | jsv | jsb | jsbq | aaa | aaa1 | exists1 | exists2 | exists3 | exists4 | js2 | jsb2w | jsb2q | ia | ta | jba +---------------------------------------------------------------------------------------+----+-----+-----+---------+---------+------+---------+---------+--------------+--------------+--------------+------+------+--------------+--------------+-----+------+---------+---------+---------+---------+--------------+----------------+--------------+----+----+----- + 1 | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [] | | | | | | | | | | | | | | | | | | | | | | | | | | | + {} | 1 | 1 | | | | | | | {} | {} | {} | {} | {} | {} | {} | | | f | 0 | | false | {} | [{}] | {} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 2 | 2 | 1 | 1.23 | 1.23 | | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | | | f | 0 | | false | 1.23 | [1.23] | 1.23 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 3 | 3 | 2 | 2 | 2 | | 2 | 2 | "2" | "2" | "2" | "2" | "2" | "2" | 2 | | | f | 0 | | false | "2" | ["2"] | 2 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 4 | 4 | | aaaaaaa | aaaa | | | aaaaaaa | "aaaaaaa" | "aaaaaaa" | "aaaaaaa" | "aaa | "aaa | "aaaaaaa" | | | | f | 0 | | false | "aaaaaaa" | ["aaaaaaa"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 5 | 5 | | foo | foo | | | | "foo" | "foo" | "foo" | "foo | "foo | "foo" | | | | f | 0 | | false | "foo" | ["foo"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 6 | 6 | | | | | | | null | null | null | null | null | null | null | | | f | 0 | | false | null | [null] | null | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 7 | 7 | 0 | false | fals | f | | false | false | false | false | fals | fals | false | false | | | f | 0 | | false | false | [false] | false | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 8 | 8 | 1 | true | true | t | | true | true | true | true | true | true | true | true | | | f | 0 | | false | true | [true] | true | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 9 | 9 | | | | | | | {"aaa": 123} | {"aaa": 123} | {"aaa": 123} | {"aa | {"aa | {"aaa": 123} | {"aaa": 123} | 123 | 123 | t | 1 | 1 | true | {"aaa": 123} | [{"aaa": 123}] | {"aaa": 123} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 10 | 10 | | [1,2] | [1,2 | | | [1,2] | "[1,2]" | "[1,2]" | "[1,2]" | "[1, | "[1, | "[1,2]" | [1, 2] | | | f | 0 | | false | "[1,2]" | ["[1,2]"] | [1, 2] | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 11 | 11 | | "str" | "str | | | "str" | "\"str\"" | "\"str\"" | "\"str\"" | "\"s | "\"s | "\"str\"" | "str" | | | f | 0 | | false | "\"str\"" | ["\"str\""] | "str" | | | +(14 rows) + +-- JSON_TABLE: Test backward parsing +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); +\sv jsonb_table_view +CREATE OR REPLACE VIEW public.jsonb_table_view AS + SELECT id, + id2, + "int", + text, + "char(4)", + bool, + "numeric", + domain, + js, + jb, + jst, + jsc, + jsv, + jsb, + jsbq, + aaa, + aaa1, + exists1, + exists2, + exists3, + js2, + jsb2w, + jsb2q, + ia, + ta, + jba, + a1, + b1, + a11, + a21, + a22 + FROM JSON_TABLE( + 'null'::jsonb, '$[*]' AS json_table_path_1 + PASSING + 1 + 2 AS a, + '"foo"'::json AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, + "int" integer PATH '$', + text text PATH '$', + "char(4)" character(4) PATH '$', + bool boolean PATH '$', + "numeric" numeric PATH '$', + domain jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc character(4) FORMAT JSON PATH '$', + jsv character varying(4) FORMAT JSON PATH '$', + jsb jsonb PATH '$', + jsbq jsonb PATH '$' OMIT QUOTES, + aaa integer PATH '$."aaa"', + aaa1 integer PATH '$."aaa"', + exists1 boolean EXISTS PATH '$."aaa"', + exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia integer[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 + COLUMNS ( + a1 integer PATH '$."a1"', + b1 text PATH '$."b1"', + NESTED PATH '$[*]' AS "p1 1" + COLUMNS ( + a11 text PATH '$."a11"' + ) + ), + NESTED PATH '$[2]' AS p2 + COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" + COLUMNS ( + a21 text PATH '$."a21"' + ), + NESTED PATH '$[*]' AS p22 + COLUMNS ( + a22 text PATH '$."a22"' + ) + ) + ) + PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22)))) + ) +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Table Function Scan on "json_table" + Output: "json_table".id, "json_table".id2, "json_table"."int", "json_table".text, "json_table"."char(4)", "json_table".bool, "json_table"."numeric", "json_table".domain, "json_table".js, "json_table".jb, "json_table".jst, "json_table".jsc, "json_table".jsv, "json_table".jsb, "json_table".jsbq, "json_table".aaa, "json_table".aaa1, "json_table".exists1, "json_table".exists2, "json_table".exists3, "json_table".js2, "json_table".jsb2w, "json_table".jsb2q, "json_table".ia, "json_table".ta, "json_table".jba, "json_table".a1, "json_table".b1, "json_table".a11, "json_table".a21, "json_table".a22 + Table Function Call: JSON_TABLE('null'::jsonb, '$[*]' AS json_table_path_1 PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (id FOR ORDINALITY, id2 FOR ORDINALITY, "int" integer PATH '$', text text PATH '$', "char(4)" character(4) PATH '$', bool boolean PATH '$', "numeric" numeric PATH '$', domain jsonb_test_domain PATH '$', js json PATH '$', jb jsonb PATH '$', jst text FORMAT JSON PATH '$', jsc character(4) FORMAT JSON PATH '$', jsv character varying(4) FORMAT JSON PATH '$', jsb jsonb PATH '$', jsbq jsonb PATH '$' OMIT QUOTES, aaa integer PATH '$."aaa"', aaa1 integer PATH '$."aaa"', exists1 boolean EXISTS PATH '$."aaa"', exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, js2 json PATH '$', jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, jsb2q jsonb PATH '$' OMIT QUOTES, ia integer[] PATH '$', ta text[] PATH '$', jba jsonb[] PATH '$', NESTED PATH '$[1]' AS p1 COLUMNS (a1 integer PATH '$."a1"', b1 text PATH '$."b1"', NESTED PATH '$[*]' AS "p1 1" COLUMNS (a11 text PATH '$."a11"')), NESTED PATH '$[2]' AS p2 COLUMNS ( NESTED PATH '$[*]' AS "p2:1" COLUMNS (a21 text PATH '$."a21"'), NESTED PATH '$[*]' AS p22 COLUMNS (a22 text PATH '$."a22"'))) PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22))))) +(3 rows) + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + js | a +-------+--- + 1 | 1 + "err" | +(2 rows) + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; + a +--- + +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: jsonpath member accessor can only be applied to an object +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: no SQL/JSON item +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 1 +(1 row) + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); + a +--- + 0 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to smallint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to bigint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to real +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 E... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); + a +----- + fal +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to json +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to jsonb +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EX... + ^ +-- JSON_TABLE: nested paths and plans +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb '[]', '$' -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 4: NESTED PATH '$' COLUMNS ( -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: b +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +-- JSON_TABLE: plan validation +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p1) + ^ +DETAIL: Path name mismatch: expected p0 but p1 is given. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 UNION p1 UNION p11) + ^ +DETAIL: Expected INNER or OUTER JSON_TABLE plan node. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 8: NESTED PATH '$' AS p2 COLUMNS ( + ^ +DETAIL: Plan node for nested path p2 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 5: NESTED PATH '$' AS p11 COLUMNS ( foo int ), + ^ +DETAIL: Plan node for nested path p11 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) + ^ +DETAIL: Plan node contains some extra or duplicate sibling nodes. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 6: NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ^ +DETAIL: Plan node for nested path p12 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 9: NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ^ +DETAIL: Plan node for nested path p21 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + bar | foo | baz +-----+-----+----- +(0 rows) + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb 'null', 'strict $[*]' -- without root path name + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- JSON_TABLE: plan execution +CREATE TEMP TABLE jsonb_table_test (js jsonb); +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + n | a | c | b +---+----+----+--- + 1 | 1 | | + 2 | 2 | 10 | + 2 | 2 | | + 2 | 2 | 20 | + 2 | 2 | | 1 + 2 | 2 | | 2 + 2 | 2 | | 3 + 3 | 3 | | 1 + 3 | 3 | | 2 + 4 | -1 | | 1 + 4 | -1 | | 2 +(11 rows) + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + n | a | b | b1 | c | c1 | b +---+---+--------------+-----+------+----+----- + 1 | 1 | [1, 10] | 1 | 1 | | 101 + 1 | 1 | [1, 10] | 1 | null | | 101 + 1 | 1 | [1, 10] | 1 | 2 | | 101 + 1 | 1 | [1, 10] | 10 | 1 | | 110 + 1 | 1 | [1, 10] | 10 | null | | 110 + 1 | 1 | [1, 10] | 10 | 2 | | 110 + 1 | 1 | [2] | 2 | 1 | | 102 + 1 | 1 | [2] | 2 | null | | 102 + 1 | 1 | [2] | 2 | 2 | | 102 + 1 | 1 | [3, 30, 300] | 3 | 1 | | 103 + 1 | 1 | [3, 30, 300] | 3 | null | | 103 + 1 | 1 | [3, 30, 300] | 3 | 2 | | 103 + 1 | 1 | [3, 30, 300] | 30 | 1 | | 130 + 1 | 1 | [3, 30, 300] | 30 | null | | 130 + 1 | 1 | [3, 30, 300] | 30 | 2 | | 130 + 1 | 1 | [3, 30, 300] | 300 | 1 | | 400 + 1 | 1 | [3, 30, 300] | 300 | null | | 400 + 1 | 1 | [3, 30, 300] | 300 | 2 | | 400 + 2 | 2 | | | | | + 3 | | | | | | +(20 rows) + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + x | y | y | z +---+---+--------------+--- + 2 | 1 | [1, 2, 3] | 1 + 2 | 1 | [1, 2, 3] | 2 + 2 | 1 | [1, 2, 3] | 3 + 3 | 1 | [1, 2, 3] | 1 + 3 | 1 | [1, 2, 3] | 2 + 3 | 1 | [1, 2, 3] | 3 + 3 | 1 | [2, 3, 4, 5] | 2 + 3 | 1 | [2, 3, 4, 5] | 3 + 3 | 1 | [2, 3, 4, 5] | 4 + 3 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [1, 2, 3] | 1 + 4 | 1 | [1, 2, 3] | 2 + 4 | 1 | [1, 2, 3] | 3 + 4 | 1 | [2, 3, 4, 5] | 2 + 4 | 1 | [2, 3, 4, 5] | 3 + 4 | 1 | [2, 3, 4, 5] | 4 + 4 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [3, 4, 5, 6] | 3 + 4 | 1 | [3, 4, 5, 6] | 4 + 4 | 1 | [3, 4, 5, 6] | 5 + 4 | 1 | [3, 4, 5, 6] | 6 + 2 | 2 | [1, 2, 3] | 2 + 2 | 2 | [1, 2, 3] | 3 + 3 | 2 | [1, 2, 3] | 2 + 3 | 2 | [1, 2, 3] | 3 + 3 | 2 | [2, 3, 4, 5] | 2 + 3 | 2 | [2, 3, 4, 5] | 3 + 3 | 2 | [2, 3, 4, 5] | 4 + 3 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [1, 2, 3] | 2 + 4 | 2 | [1, 2, 3] | 3 + 4 | 2 | [2, 3, 4, 5] | 2 + 4 | 2 | [2, 3, 4, 5] | 3 + 4 | 2 | [2, 3, 4, 5] | 4 + 4 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [3, 4, 5, 6] | 3 + 4 | 2 | [3, 4, 5, 6] | 4 + 4 | 2 | [3, 4, 5, 6] | 5 + 4 | 2 | [3, 4, 5, 6] | 6 + 2 | 3 | [1, 2, 3] | 3 + 3 | 3 | [1, 2, 3] | 3 + 3 | 3 | [2, 3, 4, 5] | 3 + 3 | 3 | [2, 3, 4, 5] | 4 + 3 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [1, 2, 3] | 3 + 4 | 3 | [2, 3, 4, 5] | 3 + 4 | 3 | [2, 3, 4, 5] | 4 + 4 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [3, 4, 5, 6] | 3 + 4 | 3 | [3, 4, 5, 6] | 4 + 4 | 3 | [3, 4, 5, 6] | 5 + 4 | 3 | [3, 4, 5, 6] | 6 +(52 rows) + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; +ERROR: could not find jsonpath variable "x" +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); + json_value +------------ + foo +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_query +------------ + 123 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); + json_query +------------ + [123] +(1 row) + +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +ERROR: syntax error at or near " " of jsonpath input +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); +ERROR: only string constants supported in JSON_TABLE path specification +LINE 1: SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || '... + ^ diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql index 4f30fa46b9..df4a430d88 100644 --- a/src/test/regress/sql/json_sqljson.sql +++ b/src/test/regress/sql/json_sqljson.sql @@ -9,3 +9,7 @@ SELECT JSON_VALUE(NULL FORMAT JSON, '$'); -- JSON_QUERY SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + +-- JSON_TABLE + +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql index b8a6148b7b..fbd86f2084 100644 --- a/src/test/regress/sql/jsonb_sqljson.sql +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -325,3 +325,632 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; + +-- JSON_TABLE + +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); + +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); + +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); + +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + +-- JSON_TABLE: Test backward parsing + +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); + +\sv jsonb_table_view + +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; + +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); + +-- JSON_TABLE: nested paths and plans + +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; + +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; + +-- JSON_TABLE: plan validation + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; + +-- JSON_TABLE: plan execution + +CREATE TEMP TABLE jsonb_table_test (js jsonb); + +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); + +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; + +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e9f34de8b8..57a6c49249 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1310,6 +1310,7 @@ JsonPathMutableContext JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback +JsonPathSpec JsonPathString JsonPathVarCallback JsonPathVariable @@ -1319,6 +1320,17 @@ JsonReturning JsonScalarExpr JsonSemAction JsonSerializeExpr +JsonTable +JsonTableColumn +JsonTableColumnType +JsonTableContext +JsonTableJoinState +JsonTableParent +JsonTablePlan +JsonTablePlanJoinType +JsonTablePlanType +JsonTableScanState +JsonTableSibling JsonTokenType JsonTransformStringValuesAction JsonTypeCategory @@ -2775,6 +2787,7 @@ TableFunc TableFuncRoutine TableFuncScan TableFuncScanState +TableFuncType TableInfo TableLikeClause TableSampleClause -- 2.35.3
v10-0004-SQL-JSON-query-functions.patch (application/octet-stream, 209.4K) download From 885e5ce57a93c31bea5fc077aa06ec16103befae Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Tue, 18 Jul 2023 17:58:35 +0900 Subject: [PATCH v10 4/6] SQL/JSON query functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This introduces the SQL/JSON functions for querying JSON data using jsonpath expressions. The functions are: JSON_EXISTS() JSON_QUERY() JSON_VALUE() All of these functions only operate on jsonb. The workaround for now is to cast the argument to jsonb. JSON_EXISTS() tests if the jsonpath expression applied to the jsonb value yields any values. JSON_VALUE() must return a single value, and an error occurs if it tries to return multiple values. JSON_QUERY() must return a json object or array, and there are various WRAPPER options for handling scalar or multi-value results. Both these functions have options for handling EMPTY and ERROR conditions. Author: Nikita Glukhov <n.gluhov@postgrespro.ru> Author: Teodor Sigaev <teodor@sigaev.ru> Author: Oleg Bartunov <obartunov@gmail.com> Author: Alexander Korotkov <aekorotkov@gmail.com> Author: Andrew Dunstan <andrew@dunslane.net> Author: Amit Langote <amitlangote09@gmail.com> Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- doc/src/sgml/func.sgml | 147 +++ src/backend/executor/execExpr.c | 432 ++++++++ src/backend/executor/execExprInterp.c | 502 ++++++++- src/backend/jit/llvm/llvmjit_expr.c | 250 +++++ src/backend/jit/llvm/llvmjit_types.c | 5 + src/backend/nodes/makefuncs.c | 15 + src/backend/nodes/nodeFuncs.c | 183 ++++ src/backend/optimizer/path/costsize.c | 3 +- src/backend/optimizer/util/clauses.c | 19 + src/backend/parser/gram.y | 348 ++++++- src/backend/parser/parse_collate.c | 7 + src/backend/parser/parse_expr.c | 516 ++++++++- src/backend/parser/parse_target.c | 15 + src/backend/utils/adt/formatting.c | 44 + src/backend/utils/adt/jsonb.c | 62 ++ src/backend/utils/adt/jsonfuncs.c | 170 ++- src/backend/utils/adt/jsonpath.c | 255 +++++ src/backend/utils/adt/jsonpath_exec.c | 391 ++++++- src/backend/utils/adt/ruleutils.c | 136 +++ src/include/executor/execExpr.h | 172 +++ src/include/executor/executor.h | 1 + src/include/nodes/execnodes.h | 3 + src/include/nodes/makefuncs.h | 1 + src/include/nodes/parsenodes.h | 48 + src/include/nodes/primnodes.h | 109 ++ src/include/parser/kwlist.h | 11 + src/include/utils/formatting.h | 2 +- src/include/utils/jsonb.h | 3 + src/include/utils/jsonfuncs.h | 5 + src/include/utils/jsonpath.h | 27 + src/interfaces/ecpg/preproc/ecpg.trailer | 28 + src/test/regress/expected/json_sqljson.out | 18 + src/test/regress/expected/jsonb_sqljson.out | 1042 +++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/json_sqljson.sql | 11 + src/test/regress/sql/jsonb_sqljson.sql | 327 ++++++ src/tools/pgindent/typedefs.list | 20 + 37 files changed, 5239 insertions(+), 91 deletions(-) create mode 100644 src/test/regress/expected/json_sqljson.out create mode 100644 src/test/regress/expected/jsonb_sqljson.out create mode 100644 src/test/regress/sql/json_sqljson.sql create mode 100644 src/test/regress/sql/jsonb_sqljson.sql diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index feb39e5352..cc16006628 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16988,6 +16988,153 @@ array w/o UK? | t </tbody> </tgroup> </table> + + <para> + <xref linkend="functions-sqljson-querying"/> details the SQL/JSON + functions that can be used to query JSON data. + </para> + + <note> + <para> + SQL/JSON paths can only be applied to the <type>jsonb</type> type, so it + might be necessary to cast the <replaceable>context_item</replaceable> + argument of these functions to <type>jsonb</type>. + </para> + </note> + + <table id="functions-sqljson-querying"> + <title>SQL/JSON Query Functions</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="func_table_entry"><para role="func_signature"> + Function signature + </para> + <para> + Description + </para> + <para> + Example(s) + </para></entry> + </row> + </thead> + <tbody> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_exists</primary></indexterm> + <function>json_exists</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>TRUE</literal> | <literal>FALSE</literal> |<literal> UNKNOWN</literal> | <literal>ERROR</literal> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns true if the SQL/JSON <replaceable>path_expression</replaceable> + applied to the <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s yields any items. + The <literal>ON ERROR</literal> clause specifies what is returned if + an error occurs. Note that if the <replaceable>path_expression</replaceable> + is <literal>strict</literal>, an error is generated if it yields no items. + The default value is <literal>UNKNOWN</literal> which causes a NULL + result. + </para> + <para> + <literal>json_exists(jsonb '{"key1": [1,2,3]}', 'strict $.key1[*] ? (@ > 2)')</literal> + <returnvalue>t</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'lax $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>f</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'strict $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>ERROR: jsonpath array subscript is out of bounds</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_value</primary></indexterm> + <function>json_value</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> + <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. The extracted value must be + a single <acronym>SQL/JSON</acronym> scalar item. For results that + are objects or arrays, use the <function>json_query</function> + function instead. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + The <literal>ON EMPTY</literal> clause specifies the behavior if the + <replaceable>path_expression</replaceable> yields no value at all. + The <literal>ON ERROR</literal> clause specifies the behavior if an + error occurs as a result of <type>jsonpath</type> evaluation + (including cast to the output type) or execution of + <literal>ON EMPTY</literal> behavior (that was caused by empty result + of <type>jsonpath</type> evaluation). + </para> + <para> + <literal>json_value(jsonb '"123.45"', '$' RETURNING float)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_value(jsonb '"03:04 2015-02-01"', '$.datetime("HH24:MI&nbsp;YYYY-MM-DD")' RETURNING date)</literal> + <returnvalue>2015-02-01</returnvalue> + </para> + <para> + <literal>json_value(jsonb '[1,2]', 'strict $[*]' DEFAULT 9 ON ERROR)</literal> + <returnvalue>9</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_query</primary></indexterm> + <function>json_query</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional> + <optional> { <literal>WITHOUT</literal> | <literal>WITH</literal> { <literal>CONDITIONAL</literal> | <optional><literal>UNCONDITIONAL</literal></optional> } } <optional> <literal>ARRAY</literal> </optional> <literal>WRAPPER</literal> </optional> + <optional> { <literal>KEEP</literal> | <literal>OMIT</literal> } <literal>QUOTES</literal> <optional> <literal>ON SCALAR STRING</literal> </optional> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. + This function must return a JSON string, so if the path expression + returns multiple SQL/JSON items, you must wrap the result using the + <literal>WITH WRAPPER</literal> clause. If the wrapper is + <literal>UNCONDITIONAL</literal>, an array wrapper will always + be applied, even if the returned value is already a single JSON object + or array, but if it is <literal>CONDITIONAL</literal>, it will not be + applied to a single array or object. <literal>UNCONDITIONAL</literal> + is the default. + If the result is a scalar string, by default the value returned will have + surrounding quotes making it a valid JSON value. However, this behavior + is reversed if <literal>OMIT QUOTES</literal> is specified. + The <literal>ON ERROR</literal> and <literal>ON EMPTY</literal> + clauses have similar semantics to those clauses for + <function>json_value</function>. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + </para> + <para> + <literal>json_query(jsonb '[1,[2,3],null]', 'lax $[*][1]' WITH CONDITIONAL WRAPPER)</literal> + <returnvalue>[3]</returnvalue> + </para></entry> + </row> + </tbody> + </tgroup> + </table> + </sect2> <sect2 id="functions-sqljson-path"> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 2c62b0c9c8..b3310b85ed 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -49,6 +49,7 @@ #include "utils/builtins.h" #include "utils/datum.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -88,6 +89,16 @@ static void ExecBuildAggTransCall(ExprState *state, AggState *aggstate, FunctionCallInfo fcinfo, AggStatePerTrans pertrans, int transno, int setno, int setoff, bool ishash, bool nullcheck); +static void ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch); +static Datum GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null); +static JsonCoercionState *ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull); +static JsonItemCoercionsState *ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull); /* @@ -2411,6 +2422,14 @@ ExecInitExprRec(Expr *node, ExprState *state, break; } + case T_JsonExpr: + { + JsonExpr *jexpr = castNode(JsonExpr, node); + + ExecInitJsonExpr(jexpr, state, resv, resnull, &scratch); + break; + } + case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -4178,3 +4197,416 @@ ExecBuildParamSetEqual(TupleDesc desc, return state; } + +/* + * Push steps to evaluate a JsonExpr and its various subsidiary expressions. + */ +static void +ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch) +{ + JsonExprState *jsestate = palloc0(sizeof(JsonExprState)); + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + ListCell *argexprlc; + ListCell *argnamelc; + int skip_step_off = -1; + int passing_args_step_off = -1; + int coercion_step_off = -1; + int coercion_finish_step_off = -1; + int behavior_step_off = -1; + int onempty_expr_step_off = -1; + int onempty_jump_step_off = -1; + int onerror_expr_step_off = -1; + int onerror_jump_step_off = -1; + List *adjust_jumps = NIL; + ListCell *lc; + ExprEvalStep *as; + + jsestate->jsexpr = jexpr; + + /* + * Add steps to compute formatted_expr, pathspec, and PASSING arg + * expressions as things that must be evaluated *before* the actual JSON + * path expression. + */ + ExecInitExprRec((Expr *) jexpr->formatted_expr, state, + &pre_eval->formatted_expr.value, + &pre_eval->formatted_expr.isnull); + ExecInitExprRec((Expr *) jexpr->path_spec, state, + &pre_eval->pathspec.value, + &pre_eval->pathspec.isnull); + + /* + * Before pushing steps for PASSING args, push a step to decide whether to + * skip evaluating the args and the JSON path expression depending on + * whether either of formatted_expr and pathspec is NULL; see + * ExecEvalJsonExprSkip(). + */ + scratch->opcode = EEOP_JSONEXPR_SKIP; + scratch->d.jsonexpr_skip.jsestate = jsestate; + skip_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* PASSING args. */ + jsestate->pre_eval.args = NIL; + passing_args_step_off = state->steps_len; + forboth(argexprlc, jexpr->passing_values, + argnamelc, jexpr->passing_names) + { + Expr *argexpr = (Expr *) lfirst(argexprlc); + String *argname = lfirst_node(String, argnamelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(argname->sval); + var->typid = exprType((Node *) argexpr); + var->typmod = exprTypmod((Node *) argexpr); + + ExecInitExprRec((Expr *) argexpr, state, &var->value, &var->isnull); + + pre_eval->args = lappend(pre_eval->args, var); + } + + /* + * Step for the actual JSON path evaluation; see ExecEvalJson() and + * ExecEvalJsonExpr(). + */ + scratch->opcode = EEOP_JSONEXPR_PATH; + scratch->d.jsonexpr.jsestate = jsestate; + ExprEvalPushStep(state, scratch); + + /* + * Push steps to control the evaluation of expressions based on the result + * of JSON path evaluation. + */ + + /* + * Step to handle ON ERROR and ON EMPTY behavior; see + * ExecEvalJsonExprBehavior(). + */ + scratch->opcode = EEOP_JSONEXPR_BEHAVIOR; + scratch->d.jsonexpr_behavior.jsestate = jsestate; + behavior_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Step(s) to evaluate ON EMPTY expression */ + onempty_expr_step_off = state->steps_len; + if (jexpr->on_empty && + jexpr->on_empty->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_empty->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_empty->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_empty, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onempty_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + } + } + + /* Step(s) to evaluate ON ERROR expression */ + onerror_expr_step_off = state->steps_len; + if (jexpr->on_error && + jexpr->on_error->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_error->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_error->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_error, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onerror_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set later */ + ExprEvalPushStep(state, scratch); + } + } + + /* + * Step to handle applying coercion to the JSON item returned by + * EEOP_JSONEXPR_PATH or to the ON EMPTY/ERROR expression as + * EEOP_JSONEXPR_BEHAVIOR decides; see ExecEvalJsonExprCoercion(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Initialize coercion expression(s). */ + if (jexpr->result_coercion) + { + jsestate->result_jcstate = + ExecInitJsonCoercion(scratch, state, jexpr->result_coercion, + resv, resnull); + /* See the comment above. */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* computed later */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + if (jexpr->coercions) + { + /* + * ExecPrepareJsonItemCoercion() called by ExecEvalJsonExpr() chooses + * one for a given JSON item returned by JsonPathValue(). + */ + jsestate->item_jcstates = + ExecInitJsonItemCoercions(scratch, state, jexpr->coercions, + resv, resnull); + } + + /* + * And a step to clean up after the coercion step; see + * ExecEvalJsonExprCoercionFinish(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION_FINISH; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_finish_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* + * Adjust jump target addresses in various post-eval steps now that we + * have all the steps in place. + */ + + /* EEOP_JSONEXPR_SKIP */ + Assert(skip_step_off >= 0); + as = &state->steps[skip_step_off]; + as->d.jsonexpr_skip.jump_coercion = coercion_step_off; + as->d.jsonexpr_skip.jump_passing_args = passing_args_step_off; + + /* EEOP_JSONEXPR_BEHAVIOR */ + Assert(behavior_step_off >= 0); + as = &state->steps[behavior_step_off]; + as->d.jsonexpr_behavior.jump_onerror_expr = onerror_expr_step_off; + as->d.jsonexpr_behavior.jump_onempty_expr = onempty_expr_step_off; + as->d.jsonexpr_behavior.jump_coercion = coercion_step_off; + as->d.jsonexpr_behavior.jump_skip_coercion = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION */ + Assert(coercion_step_off >= 0); + as = &state->steps[coercion_step_off]; + as->d.jsonexpr_coercion.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION_FINISH */ + Assert(coercion_finish_step_off >= 0); + as = &state->steps[coercion_finish_step_off]; + as->d.jsonexpr_coercion_finish.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion_finish.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JUMP steps */ + + /* + * Ones after ON EMPTY and ON ERROR non-default expressions should jump to + * the coercion step. + */ + if (onempty_jump_step_off >= 0) + { + as = &state->steps[onempty_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + if (onerror_jump_step_off >= 0) + { + as = &state->steps[onerror_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + + /* The rest should jump to the end. */ + foreach(lc, adjust_jumps) + { + as = &state->steps[lfirst_int(lc)]; + as->d.jump.jumpdone = state->steps_len; + } + + /* + * Set RETURNING type's input function used by ExecEvalJsonExprCoercion(). + */ + if (jexpr->omit_quotes || + (jexpr->result_coercion && jexpr->result_coercion->via_io)) + { + Oid typinput; + FmgrInfo *finfo; + + /* lookup the result type's input function */ + getTypeInputInfo(jexpr->returning->typid, &typinput, + &jsestate->input.typioparam); + finfo = palloc0(sizeof(FmgrInfo)); + fmgr_info(typinput, finfo); + jsestate->input.finfo = finfo; + } +} + +/* + * Evaluate a JSON error/empty behavior result. + */ +static Datum +GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null) +{ + *is_null = false; + + switch (behavior->btype) + { + case JSON_BEHAVIOR_EMPTY_ARRAY: + return JsonbPGetDatum(JsonbMakeEmptyArray()); + + case JSON_BEHAVIOR_EMPTY_OBJECT: + return JsonbPGetDatum(JsonbMakeEmptyObject()); + + case JSON_BEHAVIOR_TRUE: + return BoolGetDatum(true); + + case JSON_BEHAVIOR_FALSE: + return BoolGetDatum(false); + + case JSON_BEHAVIOR_NULL: + case JSON_BEHAVIOR_UNKNOWN: + case JSON_BEHAVIOR_EMPTY: + *is_null = true; + return (Datum) 0; + + case JSON_BEHAVIOR_DEFAULT: + /* Always handled in the caller. */ + Assert(false); + return (Datum) 0; + + default: + elog(ERROR, "unrecognized SQL/JSON behavior %d", behavior->btype); + return (Datum) 0; + } +} + +/* Initialize one JsonCoercion for execution. */ +static JsonCoercionState * +ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull) +{ + JsonCoercionState *jcstate = palloc0(sizeof(JsonCoercionState)); + + jcstate->coercion = coercion; + if (coercion && coercion->expr) + { + Datum *save_innermost_caseval; + bool *save_innermost_casenull; + + jcstate->jump_eval_expr = state->steps_len; + + /* Push step(s) to compute cstate->coercion. */ + save_innermost_caseval = state->innermost_caseval; + save_innermost_casenull = state->innermost_casenull; + + state->innermost_caseval = resv; + state->innermost_casenull = resnull; + + ExecInitExprRec((Expr *) coercion->expr, state, resv, resnull); + + state->innermost_caseval = save_innermost_caseval; + state->innermost_casenull = save_innermost_casenull; + } + else + jcstate->jump_eval_expr = -1; + + return jcstate; +} + +/* + * Push steps to evaluate coercions from a given JsonItemCoercions, which + * contains all possible coercions that may need to be applied to JSON + * items coming from evaluating the parent JsonExpr. + */ +static JsonItemCoercionsState * +ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull) +{ + JsonCoercion **coercion; + JsonItemCoercionsState *item_jcstates = palloc0(sizeof(JsonItemCoercionsState)); + JsonCoercionState **item_jcstate; + ExprEvalStep *as; + List *adjust_jumps = NIL; + ListCell *lc; + + /* Push the steps of individual coercions. */ + for (coercion = &coercions->null, + item_jcstate = &item_jcstates->null; + coercion <= &coercions->composite; + coercion++, item_jcstate++) + { + *item_jcstate = ExecInitJsonCoercion(scratch, state, *coercion, + resv, resnull); + + /* Emit JUMP step to skip past other coercions' steps. */ + scratch->opcode = EEOP_JUMP; + + /* + * Remember JUMP step address to set the actual jump target address + * below. + */ + adjust_jumps = lappend_int(adjust_jumps, state->steps_len); + ExprEvalPushStep(state, scratch); + } + + foreach(lc, adjust_jumps) + { + int jump_step_id = lfirst_int(lc); + + as = &state->steps[jump_step_id]; + as->d.jump.jumpdone = state->steps_len; + } + + return item_jcstates; +} diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 47665a9cdf..1622e44b12 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -74,6 +74,7 @@ #include "utils/json.h" #include "utils/jsonb.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/timestamp.h" @@ -152,6 +153,9 @@ static TupleDesc get_cached_rowtype(Oid type_id, int32 typmod, bool *changed); static void ExecEvalRowNullInt(ExprState *state, ExprEvalStep *op, ExprContext *econtext, bool checkisnull); +static Datum ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate); /* fast-path evaluation functions */ static Datum ExecJustInnerVar(ExprState *state, ExprContext *econtext, bool *isnull); @@ -480,6 +484,11 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_XMLEXPR, &&CASE_EEOP_JSON_CONSTRUCTOR, &&CASE_EEOP_IS_JSON, + &&CASE_EEOP_JSONEXPR_SKIP, + &&CASE_EEOP_JSONEXPR_PATH, + &&CASE_EEOP_JSONEXPR_BEHAVIOR, + &&CASE_EEOP_JSONEXPR_COERCION, + &&CASE_EEOP_JSONEXPR_COERCION_FINISH, &&CASE_EEOP_AGGREF, &&CASE_EEOP_GROUPING_FUNC, &&CASE_EEOP_WINDOW_FUNC, @@ -1186,8 +1195,14 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) /* second and third arguments are already set up */ fcinfo_in->isnull = false; + + /* Pass down soft-error capture node if any. */ + fcinfo_in->context = state->escontext; + d = FunctionCallInvoke(fcinfo_in); *op->resvalue = d; + if (SOFT_ERROR_OCCURRED(state->escontext)) + *op->resnull = true; /* Should get null result if and only if str is NULL */ if (str == NULL) @@ -1195,7 +1210,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) Assert(*op->resnull); Assert(fcinfo_in->isnull); } - else + else if (!SOFT_ERROR_OCCURRED(state->escontext)) { Assert(!*op->resnull); Assert(!fcinfo_in->isnull); @@ -1543,6 +1558,38 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_JSONEXPR_PATH) + { + /* too complex for an inline implementation */ + ExecEvalJsonExpr(state, op, econtext); + EEO_NEXT(); + } + + EEO_CASE(EEOP_JSONEXPR_SKIP) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprSkip(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_BEHAVIOR) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprBehavior(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercion(state, op, econtext, + *op->resvalue, *op->resnull)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION_FINISH) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercionFinish(state, op)); + } + EEO_CASE(EEOP_AGGREF) { /* @@ -3745,7 +3792,7 @@ ExecEvalConstraintCheck(ExprState *state, ExprEvalStep *op) { if (!*op->d.domaincheck.checknull && !DatumGetBool(*op->d.domaincheck.checkvalue)) - ereport(ERROR, + errsave(state->escontext, (errcode(ERRCODE_CHECK_VIOLATION), errmsg("value for domain %s violates check constraint \"%s\"", format_type_be(op->d.domaincheck.resulttype), @@ -4138,6 +4185,457 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op) *op->resvalue = BoolGetDatum(res); } +/* + * Evaluate given JsonExpr by performing the specified JSON operation. + * + * This also populates the JsonExprPostEvalState with the information needed + * by the subsequent steps that handle the specified JsonBehavior. + */ +void +ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) +{ + JsonExprState *jsestate = op->d.jsonexpr.jsestate; + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonExpr *jexpr = jsestate->jsexpr; + Datum item; + Datum res = (Datum) 0; + bool resnull = true; + JsonPath *path; + bool *error; + bool *empty; + + item = pre_eval->formatted_expr.value; + path = DatumGetJsonPathP(pre_eval->pathspec.value); + + /* Reset JsonExprPostEvalState for this evaluation. */ + memset(post_eval, 0, sizeof(*post_eval)); + + /* Respect ON ERROR behavior during path evaluation. */ + jsestate->throw_error = (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR); + + /* + * Be sure to save the error (if needed) and empty statuses for the + * EEOP_JSONEXPR_BEHAVIOR step to peruse. + */ + error = !jsestate->throw_error ? &post_eval->error : NULL; + empty = &post_eval->empty; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + res = JsonPathQuery(item, path, jexpr->wrapper, empty, error, + pre_eval->args); + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + resnull = !DatumGetPointer(res); + break; + + case JSON_VALUE_OP: + { + JsonbValue *jbv = JsonPathValue(item, path, empty, error, + pre_eval->args); + + if (error && *error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + if (!jbv) /* NULL or empty */ + { + resnull = true; + break; + } + + Assert(!*empty); + + resnull = false; + + /* coerce scalar item to the output type */ + if (jexpr->returning->typid == JSONOID || + jexpr->returning->typid == JSONBOID) + { + /* Use result coercion from json[b] to the output type */ + res = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + break; + } + + /* + * Error out if no cast exists to coerce SQL/JSON item to the + * the output type. + */ + Assert(post_eval->item_jcstate == NULL); + res = ExecPrepareJsonItemCoercion(jbv, + jsestate->item_jcstates, + &post_eval->item_jcstate); + if (post_eval->item_jcstate && + post_eval->item_jcstate->coercion && + (post_eval->item_jcstate->coercion->via_io || + post_eval->item_jcstate->coercion->via_populate)) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * Coercion via I/O means here that the cast to the target + * type simply does not exist. + */ + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE), + errmsg("SQL/JSON item cannot be cast to target type"))); + } + break; + } + + case JSON_EXISTS_OP: + { + bool exists = JsonPathExists(item, path, + pre_eval->args, + error); + + resnull = error && *error; + res = BoolGetDatum(exists); + break; + } + + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * If the ON EMPTY behavior is to cause an error, do so here. Other + * behaviors will be handled by the caller. + */ + if (*empty) + { + Assert(jexpr->on_empty); /* it is not JSON_EXISTS */ + + if (jexpr->on_empty->btype == JSON_BEHAVIOR_ERROR) + { + if (error) + { + *error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + ereport(ERROR, + (errcode(ERRCODE_NO_SQL_JSON_ITEM), + errmsg("no SQL/JSON item"))); + } + } + + *op->resvalue = res; + *op->resnull = resnull; +} + +/* + * Skip calling ExecEvalJson() on the given JsonExpr? + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_skip.jsestate; + + /* + * Skip if either of the input expressions has turned out to be NULL, + * though do execute domain checks for NULLs, which are handled by the + * coercion step. + */ + if (jsestate->pre_eval.formatted_expr.isnull || + jsestate->pre_eval.pathspec.isnull) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_skip.jump_coercion; + } + + /* + * Go evaluate the PASSING args if any and subsequently JSON path itself. + */ + return op->d.jsonexpr_skip.jump_passing_args; +} + +/* + * Returns the step address to perform the JsonBehavior applicable to + * the JSON item that resulted from evaluating the given JsonExpr. + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_behavior.jsestate; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonBehavior *behavior = NULL; + int jump_to = -1; + + if (post_eval->error || post_eval->coercion_error) + { + behavior = jsestate->jsexpr->on_error; + jump_to = op->d.jsonexpr_behavior.jump_onerror_expr; + } + else if (post_eval->empty) + { + behavior = jsestate->jsexpr->on_empty; + jump_to = op->d.jsonexpr_behavior.jump_onempty_expr; + } + else if (!post_eval->coercion_done) + { + /* + * If no error or the JSON item is not empty, directly go to the + * coercion step to coerce the item as is. + */ + return op->d.jsonexpr_behavior.jump_coercion; + } + + Assert(behavior); + + /* + * Set up for coercion step that will run to coerce a non-default behavior + * value. It should use result_coercion, if any. Errors that may occur + * should be thrown for JSON ops other than JSON_VALUE_OP. + */ + if (behavior->btype != JSON_BEHAVIOR_DEFAULT) + { + post_eval->item_jcstate = NULL; + jsestate->throw_error = (jsestate->jsexpr->op != JSON_VALUE_OP); + } + + Assert(jump_to >= 0); + return jump_to; +} + +/* + * Evaluate or return the step address to evaluate a coercion of a JSON item + * to the target type. The former if the coercion must be done right away by + * calling its input function or by calling json_populate_type(). + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull) +{ + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonExpr *jexpr = jsestate->jsexpr; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonCoercionState *item_jcstate = post_eval->item_jcstate; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + + if (item_jcstate == NULL && + jsestate->jsexpr->op != JSON_EXISTS_OP) + { + ErrorSaveContext escontext = {T_ErrorSaveContext}; + Node *escontext_p; + JsonCoercion *coercion; + Jsonb *jb; + + escontext_p = !jsestate->throw_error ? (Node *) &escontext : NULL; + coercion = result_jcstate ? result_jcstate->coercion : NULL; + jb = resnull ? NULL : DatumGetJsonbP(res); + + if ((coercion && coercion->via_io) || + (jexpr->omit_quotes && !resnull && + JB_ROOT_IS_SCALAR(jb))) + { + /* strip quotes and call typinput function */ + char *str = resnull ? NULL : JsonbUnquote(jb); + bool type_is_domain; + + type_is_domain = (getBaseType(jexpr->returning->typid) != + jexpr->returning->typid); + + /* + * Catch errors only if the type is not a domain, because errors + * caused by a domain's constraint failure must be thrown right + * away. + */ + if (!InputFunctionCallSafe(jsestate->input.finfo, str, + jsestate->input.typioparam, + jexpr->returning->typmod, + !type_is_domain ? escontext_p : NULL, + op->resvalue)) + { + post_eval->error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + else if (coercion && coercion->via_populate) + { + *op->resvalue = json_populate_type(res, JSONBOID, + jexpr->returning->typid, + jexpr->returning->typmod, + &post_eval->cache, + econtext->ecxt_per_query_memory, + op->resnull, + escontext_p); + if (SOFT_ERROR_OCCURRED(escontext_p)) + { + post_eval->error = true; + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + } + + if (!jsestate->throw_error) + { + post_eval->escontext.type = T_ErrorSaveContext; + state->escontext = (Node *) &post_eval->escontext; + } + + if (item_jcstate && item_jcstate->jump_eval_expr >= 0) + return item_jcstate->jump_eval_expr; + else if (result_jcstate && result_jcstate->jump_eval_expr >= 0) + return result_jcstate->jump_eval_expr; + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; +} + +/* + * Checks if the coercion evaluation led to an error. If an error did occur, + * this returns the address of the step that handles the error, otherwise + * the step after the coercion step, which finishes the JsonExpr processing. + */ +int +ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op) +{ + JsonExprPostEvalState *post_eval = + &op->d.jsonexpr_coercion_finish.jsestate->post_eval; + + if (SOFT_ERROR_OCCURRED(state->escontext)) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + post_eval->coercion_error = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_error; + } + + /* Reset. */ + state->escontext = NULL; + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_done; +} + +/* + * Prepare SQL/JSON item coercion to the output type. Returned a datum of the + * corresponding SQL type and a pointer to the coercion state. + */ +static Datum +ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate) +{ + JsonCoercionState *item_jcstate; + Datum res; + JsonbValue buf; + + if (item->type == jbvBinary && + JsonContainerIsScalar(item->val.binary.data)) + { + bool res PG_USED_FOR_ASSERTS_ONLY; + + res = JsonbExtractScalar(item->val.binary.data, &buf); + item = &buf; + Assert(res); + } + + /* get coercion state reference and datum of the corresponding SQL type */ + switch (item->type) + { + case jbvNull: + item_jcstate = item_jcstates->null; + res = (Datum) 0; + break; + + case jbvString: + item_jcstate = item_jcstates->string; + res = PointerGetDatum(cstring_to_text_with_len(item->val.string.val, + item->val.string.len)); + break; + + case jbvNumeric: + item_jcstate = item_jcstates->numeric; + res = NumericGetDatum(item->val.numeric); + break; + + case jbvBool: + item_jcstate = item_jcstates->boolean; + res = BoolGetDatum(item->val.boolean); + break; + + case jbvDatetime: + res = item->val.datetime.value; + switch (item->val.datetime.typid) + { + case DATEOID: + item_jcstate = item_jcstates->date; + break; + case TIMEOID: + item_jcstate = item_jcstates->time; + break; + case TIMETZOID: + item_jcstate = item_jcstates->timetz; + break; + case TIMESTAMPOID: + item_jcstate = item_jcstates->timestamp; + break; + case TIMESTAMPTZOID: + item_jcstate = item_jcstates->timestamptz; + break; + default: + elog(ERROR, "unexpected jsonb datetime type oid %u", + item->val.datetime.typid); + return (Datum) 0; + } + break; + + case jbvArray: + case jbvObject: + case jbvBinary: + item_jcstate = item_jcstates->composite; + res = JsonbPGetDatum(JsonbValueToJsonb(item)); + break; + + default: + elog(ERROR, "unexpected jsonb value type %d", item->type); + return (Datum) 0; + } + + *p_item_jcstate = item_jcstate; + + return res; +} + /* * ExecEvalGroupingFunc diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 00d7b8110b..4f155ab0f3 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -1860,6 +1860,256 @@ llvm_compile_expr(ExprState *state) LLVMBuildBr(b, opblocks[opno + 1]); break; + case EEOP_JSONEXPR_PATH: + build_EvalXFunc(b, mod, "ExecEvalJsonExpr", + v_state, op, v_econtext); + LLVMBuildBr(b, opblocks[opno + 1]); + break; + + case EEOP_JSONEXPR_SKIP: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprSkip() to decide if JSON path + * evaluation can be skipped. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprSkip"), + params, lengthof(params), ""); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_skip.jump_coercion, which signifies + * skipping of JSON path evaluation, else to the next step + * which must point to the steps to evaluate PASSING args, + * if any, or to the JSON path evaluation. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_skip.jump_coercion), + ""), + opblocks[op->d.jsonexpr_skip.jump_coercion], + opblocks[opno + 1]); + break; + } + + case EEOP_JSONEXPR_BEHAVIOR: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + LLVMBasicBlockRef b_jump_onerror_default; + + /* + * Call ExecEvalJsonExprBehavior() to decide if ON EMPTY + * or ON ERROR behavior must be invoked depending on what + * JSON path evaluation returned. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprBehavior"), + params, lengthof(params), ""); + + b_jump_onerror_default = + l_bb_before_v(opblocks[opno + 1], + "op.%d.jsonexpr_behavior_jump_onerror_default", opno); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_behavior.jump_coercion, else to the + * next block, one that checks whether to evaluate the ON + * ERROR default expression. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_coercion), + ""), + opblocks[op->d.jsonexpr_behavior.jump_coercion], + b_jump_onerror_default); + + /* + * Block that checks whether to evaluate the ON ERROR + * default expression. + * + * Jump to evaluate the ON ERROR default expression if the + * returned address is the same as + * jsonexpr_behavior.jump_onerror_default, else jump to + * evaluate the ON EMPTY default expression. + */ + LLVMPositionBuilderAtEnd(b, b_jump_onerror_default); + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_onerror_expr), + ""), + opblocks[op->d.jsonexpr_behavior.jump_onerror_expr], + opblocks[op->d.jsonexpr_behavior.jump_onempty_expr]); + break; + } + case EEOP_JSONEXPR_COERCION: + { + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonItemCoercionsState *item_jcstates = jsestate->item_jcstates; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + LLVMValueRef v_ret; + LLVMValueRef params[5]; + + /* + * Call ExecEvalJsonExprCoercion() to evaluate appropriate + * coercion. This will return the step address to jump + * to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + params[2] = v_econtext; + params[3] = v_resvaluep; + params[4] = v_resnullp; + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercion"), + params, lengthof(params), ""); + + /* + * Jump to handle a coercion error if the returned address + * is the same as jsonexpr_coercion.jump_coercion_error, + * else to the step after coercion (coercion done!). + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + + if (item_jcstates) + { + JsonCoercionState **item_jcstate; + int n_coercions = (int) + (item_jcstates->composite - item_jcstates->null) + 1; + int i; + LLVMBasicBlockRef *b_coercions; + + + /* + * Will create a block for each coercion below to + * check whether to evaluate the coercion's expression + * if there's one or to skip to the end if not. + */ + b_coercions = palloc((n_coercions + 1) * sizeof(LLVMBasicBlockRef)); + for (i = 0; i < n_coercions + 1; i++) + b_coercions[i] = + l_bb_before_v(opblocks[opno + 1], + "op.%d.json_item_coercion.%d", + opno, i); + + /* Jump to check first coercion */ + LLVMBuildBr(b, b_coercions[0]); + + /* + * Add conditional branches for individual coercion's + * expressions + */ + for (item_jcstate = &item_jcstates->null, i = 0; + item_jcstate <= &item_jcstates->composite; + item_jcstate++, i++) + { + /* Block for this coercion */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + + /* + * Jump to evaluate the coercion's expression if + * the address returned is the same as this + * coercion's jump_eval_expr (that is, if it is + * valid), else check the next coercion's. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const((*item_jcstate)->jump_eval_expr), + ""), + (*item_jcstate)->jump_eval_expr >= 0 ? + opblocks[(*item_jcstate)->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + b_coercions[i + 1]); + } + + /* + * A placeholder block that the last coercion's block + * might jump to, which unconditionally jumps to end + * of coercions. + */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + LLVMBuildBr(b, opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + + /* + * Jump to evaluate the result_coercion's expression if + * none of the above coercions matched, that is, if + * there's one. + */ + if (result_jcstate) + { + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(result_jcstate->jump_eval_expr), + ""), + result_jcstate->jump_eval_expr >= 0 ? + opblocks[result_jcstate->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + break; + } + + case EEOP_JSONEXPR_COERCION_FINISH: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprCoercionFinish() to check whether + * an coercion error occurred, in which case we must jump + * to whatever step handles the error. This returns the + * step address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercionFinish"), + params, lengthof(params), ""); + + /* + * Jump to the step that handles coercion error if the + * returned address is the same as + * jsonexpr_coercion_finish.jump_coercion_error, else to + * jsonexpr_coercion_finish.jump_coercion_done. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion_finish.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_done]); + break; + } + case EEOP_AGGREF: { LLVMValueRef v_aggno; diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c index 41ac4c6f45..cf3ced3427 100644 --- a/src/backend/jit/llvm/llvmjit_types.c +++ b/src/backend/jit/llvm/llvmjit_types.c @@ -135,6 +135,11 @@ void *referenced_functions[] = ExecEvalXmlExpr, ExecEvalJsonConstructor, ExecEvalJsonIsPredicate, + ExecEvalJsonExprSkip, + ExecEvalJsonExprBehavior, + ExecEvalJsonExprCoercion, + ExecEvalJsonExprCoercionFinish, + ExecEvalJsonExpr, MakeExpandedObjectReadOnlyInternal, slot_getmissingattrs, slot_getsomeattrs_int, diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 0e7e6e46d9..9c02634355 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -860,6 +860,21 @@ makeJsonValueExpr(Expr *raw_expr, Expr *formatted_expr, return jve; } +/* + * makeJsonBehavior - + * creates a JsonBehavior node + */ +JsonBehavior * +makeJsonBehavior(JsonBehaviorType type, Node *default_expr) +{ + JsonBehavior *behavior = makeNode(JsonBehavior); + + behavior->btype = type; + behavior->default_expr = default_expr; + + return behavior; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index c03f4f23e2..cd5fff2bbe 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -234,6 +234,12 @@ exprType(const Node *expr) case T_JsonIsPredicate: type = BOOLOID; break; + case T_JsonExpr: + type = ((const JsonExpr *) expr)->returning->typid; + break; + case T_JsonCoercion: + type = exprType(((const JsonCoercion *) expr)->expr); + break; case T_NullTest: type = BOOLOID; break; @@ -493,6 +499,10 @@ exprTypmod(const Node *expr) return exprTypmod((Node *) ((const JsonValueExpr *) expr)->formatted_expr); case T_JsonConstructorExpr: return ((const JsonConstructorExpr *) expr)->returning->typmod; + case T_JsonExpr: + return ((JsonExpr *) expr)->returning->typmod; + case T_JsonCoercion: + return exprTypmod(((const JsonCoercion *) expr)->expr); case T_CoerceToDomain: return ((const CoerceToDomain *) expr)->resulttypmod; case T_CoerceToDomainValue: @@ -969,6 +979,22 @@ exprCollation(const Node *expr) /* IS JSON's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + coll = InvalidOid; + else if (coercion->expr) + coll = exprCollation(coercion->expr); + else if (coercion->via_io || coercion->via_populate) + coll = coercion->collation; + else + coll = InvalidOid; + } + break; + case T_NullTest: /* NullTest's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ @@ -1205,6 +1231,21 @@ exprSetCollation(Node *expr, Oid collation) case T_JsonIsPredicate: Assert(!OidIsValid(collation)); /* result is always boolean */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + Assert(!OidIsValid(collation)); + else if (coercion->expr) + exprSetCollation(coercion->expr, collation); + else if (coercion->via_io || coercion->via_populate) + coercion->collation = collation; + else + Assert(!OidIsValid(collation)); + } + break; case T_NullTest: /* NullTest's result is boolean ... */ Assert(!OidIsValid(collation)); /* ... so never set a collation */ @@ -1508,6 +1549,15 @@ exprLocation(const Node *expr) case T_JsonIsPredicate: loc = ((const JsonIsPredicate *) expr)->location; break; + case T_JsonExpr: + { + const JsonExpr *jsexpr = (const JsonExpr *) expr; + + /* consider both function name and leftmost arg */ + loc = leftmostLoc(jsexpr->location, + exprLocation(jsexpr->formatted_expr)); + } + break; case T_NullTest: { const NullTest *nexpr = (const NullTest *) expr; @@ -2260,6 +2310,54 @@ expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + if (WALK(jexpr->formatted_expr)) + return true; + if (WALK(jexpr->result_coercion)) + return true; + if (WALK(jexpr->passing_values)) + return true; + /* we assume walker doesn't care about passing_names */ + if (jexpr->on_empty && + WALK(jexpr->on_empty->default_expr)) + return true; + if (WALK(jexpr->on_error->default_expr)) + return true; + if (WALK(jexpr->coercions)) + return true; + } + break; + case T_JsonCoercion: + return WALK(((JsonCoercion *) node)->expr); + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + + if (WALK(coercions->null)) + return true; + if (WALK(coercions->string)) + return true; + if (WALK(coercions->numeric)) + return true; + if (WALK(coercions->boolean)) + return true; + if (WALK(coercions->date)) + return true; + if (WALK(coercions->time)) + return true; + if (WALK(coercions->timetz)) + return true; + if (WALK(coercions->timestamp)) + return true; + if (WALK(coercions->timestamptz)) + return true; + if (WALK(coercions->composite)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: @@ -3259,6 +3357,54 @@ expression_tree_mutator_impl(Node *node, return (Node *) newnode; } + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + JsonExpr *newnode; + + FLATCOPY(newnode, jexpr, JsonExpr); + MUTATE(newnode->path_spec, jexpr->path_spec, Node *); + MUTATE(newnode->formatted_expr, jexpr->formatted_expr, Node *); + MUTATE(newnode->result_coercion, jexpr->result_coercion, JsonCoercion *); + MUTATE(newnode->passing_values, jexpr->passing_values, List *); + /* assume mutator does not care about passing_names */ + if (newnode->on_empty) + MUTATE(newnode->on_empty->default_expr, + jexpr->on_empty->default_expr, Node *); + MUTATE(newnode->on_error->default_expr, + jexpr->on_error->default_expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonCoercion: + { + JsonCoercion *coercion = (JsonCoercion *) node; + JsonCoercion *newnode; + + FLATCOPY(newnode, coercion, JsonCoercion); + MUTATE(newnode->expr, coercion->expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + JsonItemCoercions *newnode; + + FLATCOPY(newnode, coercions, JsonItemCoercions); + MUTATE(newnode->null, coercions->null, JsonCoercion *); + MUTATE(newnode->string, coercions->string, JsonCoercion *); + MUTATE(newnode->numeric, coercions->numeric, JsonCoercion *); + MUTATE(newnode->boolean, coercions->boolean, JsonCoercion *); + MUTATE(newnode->date, coercions->date, JsonCoercion *); + MUTATE(newnode->time, coercions->time, JsonCoercion *); + MUTATE(newnode->timetz, coercions->timetz, JsonCoercion *); + MUTATE(newnode->timestamp, coercions->timestamp, JsonCoercion *); + MUTATE(newnode->timestamptz, coercions->timestamptz, JsonCoercion *); + MUTATE(newnode->composite, coercions->composite, JsonCoercion *); + return (Node *) newnode; + } + break; case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -3945,6 +4091,43 @@ raw_expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonArgument: + return WALK(((JsonArgument *) node)->val); + case T_JsonCommon: + { + JsonCommon *jc = (JsonCommon *) node; + + if (WALK(jc->expr)) + return true; + if (WALK(jc->pathspec)) + return true; + if (WALK(jc->passing)) + return true; + } + break; + case T_JsonBehavior: + { + JsonBehavior *jb = (JsonBehavior *) node; + + if (jb->btype == JSON_BEHAVIOR_DEFAULT && + WALK(jb->default_expr)) + return true; + } + break; + case T_JsonFuncExpr: + { + JsonFuncExpr *jfe = (JsonFuncExpr *) node; + + if (WALK(jfe->common)) + return true; + if (jfe->output && WALK(jfe->output)) + return true; + if (WALK(jfe->on_empty)) + return true; + if (WALK(jfe->on_error)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index ef475d95a1..f58c275b4b 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -4609,7 +4609,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context) IsA(node, SQLValueFunction) || IsA(node, XmlExpr) || IsA(node, CoerceToDomain) || - IsA(node, NextValueExpr)) + IsA(node, NextValueExpr) || + IsA(node, JsonExpr)) { /* Treat all these as having cost 1 */ context->total.per_tuple += cpu_operator_cost; diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index da258968b8..e40cfab4b7 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -53,6 +53,7 @@ #include "utils/fmgroids.h" #include "utils/json.h" #include "utils/jsonb.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/syscache.h" @@ -412,6 +413,24 @@ contain_mutable_functions_walker(Node *node, void *context) /* Check all subnodes */ } + if (IsA(node, JsonExpr)) + { + JsonExpr *jexpr = castNode(JsonExpr, node); + Const *cnst; + + if (!IsA(jexpr->path_spec, Const)) + return true; + + cnst = castNode(Const, jexpr->path_spec); + + Assert(cnst->consttype == JSONPATHOID); + if (cnst->constisnull) + return false; + + return jspIsMutable(DatumGetJsonPathP(cnst->constvalue), + jexpr->passing_names, jexpr->passing_values); + } + if (IsA(node, SQLValueFunction)) { /* all variants of SQLValueFunction are stable */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 856d5dee0e..1a1614645c 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -278,6 +278,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MergeWhenClause *mergewhen; struct KeyActions *keyactions; struct KeyAction *keyaction; + JsonBehavior *jsbehavior; + JsonQuotes js_quotes; } %type <node> stmt toplevel_stmt schema_stmt routine_body_stmt @@ -650,14 +652,22 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_returning_clause_opt json_name_and_value json_aggregate_func + json_api_common_syntax + json_argument %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt + json_arguments %type <ival> json_encoding_clause_opt json_predicate_type_constraint + json_wrapper_behavior %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt +%type <jsbehavior> json_value_behavior + json_query_behavior + json_exists_behavior +%type <js_quotes> json_quotes_clause_opt /* @@ -695,7 +705,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT - COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT + COMMITTED COMPRESSION CONCURRENTLY CONDITIONAL CONFIGURATION CONFLICT CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY COST CREATE CROSS CSV CUBE CURRENT_P CURRENT_CATALOG CURRENT_DATE CURRENT_ROLE CURRENT_SCHEMA @@ -706,8 +716,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P DOUBLE_P DROP - EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT - EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION + EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE + EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION EXTENSION EXTERNAL EXTRACT FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR @@ -722,10 +732,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION - JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG - JSON_SCALAR JSON_SERIALIZE + JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE - KEY KEYS + KEY KEYS KEEP LABEL LANGUAGE LARGE_P LAST_P LATERAL_P LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL @@ -739,7 +749,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC - OBJECT_P OF OFF OFFSET OIDS OLD ON ONLY OPERATOR OPTION OPTIONS OR + OBJECT_P OF OFF OFFSET OIDS OLD OMIT ON ONLY OPERATOR OPTION OPTIONS OR ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER @@ -748,7 +758,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION - QUOTE + QUOTE QUOTES RANGE READ REAL REASSIGN RECHECK RECURSIVE REF_P REFERENCES REFERENCING REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA @@ -759,7 +769,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); SEQUENCE SEQUENCES SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P - START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P + START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRING_P STRIP_P SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN @@ -767,7 +777,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); TREAT TRIGGER TRIM TRUE_P TRUNCATE TRUSTED TYPE_P TYPES_P - UESCAPE UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN + UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING @@ -15672,6 +15682,192 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_error = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->omit_quotes = $6 == JS_QUOTES_OMIT; + n->on_empty = $7; + n->on_error = $10; + n->location = @1; + $$ = (Node *) n; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->location = @1; + $$ = (Node *) p; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + json_exists_behavior ON ERROR_P + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->on_error = $5; + p->location = @1; + $$ = (Node *) p; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->on_error = $8; + n->location = @1; + $$ = (Node *) n; + } ; @@ -16398,6 +16594,72 @@ opt_asymmetric: ASYMMETRIC ; /* SQL/JSON support */ +json_api_common_syntax: + json_value_expr ',' a_expr /* i.e. a json_path */ + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + + | json_value_expr ',' a_expr /* i.e. a json_path */ + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = $5; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = $7; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_arguments: + json_argument { $$ = list_make1($1); } + | json_arguments ',' json_argument { $$ = lappend($1, $3); } + ; + +json_argument: + json_value_expr AS ColLabel + { + JsonArgument *n = makeNode(JsonArgument); + + n->val = (JsonValueExpr *) $1; + n->name = $3; + $$ = (Node *) n; + } + ; + json_value_expr: a_expr json_format_clause_opt { @@ -16423,6 +16685,50 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; +json_query_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + | EMPTY_P ARRAY { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + | EMPTY_P OBJECT_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + /* non-standard, for Oracle compatibility only */ + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + ; + +json_exists_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | TRUE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_TRUE, NULL); } + | FALSE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_FALSE, NULL); } + | UNKNOWN { $$ = makeJsonBehavior(JSON_BEHAVIOR_UNKNOWN, NULL); } + ; + +json_value_behavior: + NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + ; + +/* ARRAY is a noise word */ +json_wrapper_behavior: + WITHOUT WRAPPER { $$ = JSW_NONE; } + | WITHOUT ARRAY WRAPPER { $$ = JSW_NONE; } + | WITH WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL ARRAY WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL WRAPPER { $$ = JSW_UNCONDITIONAL; } + | /* empty */ { $$ = JSW_NONE; } + ; + +json_quotes_clause_opt: + KEEP QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_KEEP; } + | KEEP QUOTES { $$ = JS_QUOTES_KEEP; } + | OMIT QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_OMIT; } + | OMIT QUOTES { $$ = JS_QUOTES_OMIT; } + | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } + ; + json_returning_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17025,6 +17331,7 @@ unreserved_keyword: | COMMIT | COMMITTED | COMPRESSION + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17061,10 +17368,12 @@ unreserved_keyword: | DOUBLE_P | DROP | EACH + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17114,6 +17423,7 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION + | KEEP | KEY | KEYS | LABEL @@ -17160,6 +17470,7 @@ unreserved_keyword: | OFF | OIDS | OLD + | OMIT | OPERATOR | OPTION | OPTIONS @@ -17190,6 +17501,7 @@ unreserved_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REASSIGN @@ -17249,6 +17561,7 @@ unreserved_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUPPORT @@ -17271,6 +17584,7 @@ unreserved_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNKNOWN | UNLISTEN @@ -17331,10 +17645,13 @@ col_name_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE | LEAST | NATIONAL | NCHAR @@ -17567,6 +17884,7 @@ bare_label_keyword: | COMMITTED | COMPRESSION | CONCURRENTLY + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17619,11 +17937,13 @@ bare_label_keyword: | DROP | EACH | ELSE + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | END_P | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17693,10 +18013,14 @@ bare_label_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE + | KEEP | KEY | KEYS | LABEL @@ -17757,6 +18081,7 @@ bare_label_keyword: | OFF | OIDS | OLD + | OMIT | ONLY | OPERATOR | OPTION @@ -17794,6 +18119,7 @@ bare_label_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REAL @@ -17862,6 +18188,7 @@ bare_label_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUBSTRING @@ -17896,6 +18223,7 @@ bare_label_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNIQUE | UNKNOWN diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c index 9f6afc351c..692e5d1225 100644 --- a/src/backend/parser/parse_collate.c +++ b/src/backend/parser/parse_collate.c @@ -691,6 +691,13 @@ assign_collations_walker(Node *node, assign_collations_context *context) &loccontext); } break; + case T_JsonExpr: + + /* + * Context item and PASSING arguments are already + * marked with collations in parse_expr.c. + */ + break; default: /* diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 12f8777bfd..e8ac4eeaf3 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -90,6 +90,7 @@ static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); static Node *transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr *expr); +static Node *transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *p); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -353,6 +354,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); break; + case T_JsonFuncExpr: + result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3229,7 +3234,7 @@ makeJsonByteaToTextConversion(Node *expr, JsonFormat *format, int location) static Node * transformJsonValueExpr(ParseState *pstate, const char *constructName, JsonValueExpr *ve, JsonFormatType default_format, - Oid targettype) + Oid targettype, bool isarg) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3266,6 +3271,35 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, else format = ve->format->format_type; } + else if (isarg) + { + /* Pass SQL/JSON item types directly without conversion to json[b]. */ + switch (exprtype) + { + case TEXTOID: + case NUMERICOID: + case BOOLOID: + case INT2OID: + case INT4OID: + case INT8OID: + case FLOAT4OID: + case FLOAT8OID: + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + return expr; + + default: + if (typcategory == TYPCATEGORY_STRING) + return expr; + /* else convert argument to json[b] type */ + break; + } + + format = default_format; + } else if (exprtype == JSONOID || exprtype == JSONBOID) format = JS_FORMAT_DEFAULT; /* do not format json[b] types */ else @@ -3277,7 +3311,8 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, Node *coerced; bool only_allow_cast = OidIsValid(targettype); - if (!only_allow_cast && + if (!isarg && + !only_allow_cast && exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), @@ -3626,7 +3661,7 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) Node *val = transformJsonValueExpr(pstate, "JSON_OBJECT()", kv->value, JS_FORMAT_DEFAULT, - InvalidOid); + InvalidOid, false); args = lappend(args, key); args = lappend(args, val); @@ -3813,7 +3848,7 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", agg->arg->value, JS_FORMAT_DEFAULT, - InvalidOid); + InvalidOid, false); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3869,9 +3904,8 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) Oid aggfnoid; Oid aggtype; - arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", - agg->arg, - JS_FORMAT_DEFAULT, InvalidOid); + arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", agg->arg, + JS_FORMAT_DEFAULT, InvalidOid, false); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3918,9 +3952,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) { JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, "JSON_ARRAY()", - jsval, - JS_FORMAT_DEFAULT, - InvalidOid); + jsval, JS_FORMAT_DEFAULT, + InvalidOid, false); args = lappend(args, val); } @@ -4079,7 +4112,7 @@ transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) * function-like CASTs. */ arg = transformJsonValueExpr(pstate, "JSON()", jsexpr->expr, - JS_FORMAT_JSON, returning->typid); + JS_FORMAT_JSON, returning->typid, false); } return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, @@ -4124,7 +4157,7 @@ transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr *expr) Node *arg = transformJsonValueExpr(pstate, "JSON_SERIALIZE()", expr->expr, JS_FORMAT_JSON, - InvalidOid); + InvalidOid, false); if (expr->output) { @@ -4158,3 +4191,462 @@ transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr *expr) return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), NULL, returning, false, false, expr->location); } + +/* + * Transform a JSON PASSING clause. + */ +static void +transformJsonPassingArgs(ParseState *pstate, char *constructName, + JsonFormatType format, List *args, + List **passing_values, List **passing_names) +{ + ListCell *lc; + + *passing_values = NIL; + *passing_names = NIL; + + foreach(lc, args) + { + JsonArgument *arg = castNode(JsonArgument, lfirst(lc)); + Node *expr = transformJsonValueExpr(pstate, constructName, + arg->val, format, + InvalidOid, true); + + assign_expr_collations(pstate, expr); + + *passing_values = lappend(*passing_values, expr); + *passing_names = lappend(*passing_names, makeString(arg->name)); + } +} + +/* + * Transform a JSON BEHAVIOR clause. + */ +static JsonBehavior * +transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior, + JsonBehaviorType default_behavior) +{ + JsonBehaviorType behavior_type = default_behavior; + Node *default_expr = NULL; + + if (behavior) + { + behavior_type = behavior->btype; + if (behavior_type == JSON_BEHAVIOR_DEFAULT) + default_expr = transformExprRecurse(pstate, behavior->default_expr); + } + return makeJsonBehavior(behavior_type, default_expr); +} + +/* + * Common code for JSON_VALUE, JSON_QUERY, JSON_EXISTS transformation + * into a JsonExpr node. + */ +static JsonExpr * +transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = makeNode(JsonExpr); + Node *pathspec; + JsonFormatType format; + char *constructName; + + if (func->common->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("JSON_TABLE path name is not allowed here"), + parser_errposition(pstate, func->location))); + + jsexpr->location = func->location; + jsexpr->op = func->op; + switch (jsexpr->op) + { + case JSON_VALUE_OP: + constructName = "JSON_VALUE()"; + break; + case JSON_QUERY_OP: + constructName = "JSON_QUERY()"; + break; + case JSON_EXISTS_OP: + constructName = "JSON_EXISTS()"; + break; + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); + break; + } + jsexpr->formatted_expr = transformJsonValueExpr(pstate, constructName, + func->common->expr, + JS_FORMAT_DEFAULT, + InvalidOid, false); + + assign_expr_collations(pstate, jsexpr->formatted_expr); + + /* format is determined by context item type */ + format = exprType(jsexpr->formatted_expr) == JSONBOID ? JS_FORMAT_JSONB : JS_FORMAT_JSON; + + jsexpr->result_coercion = NULL; + jsexpr->omit_quotes = false; + + jsexpr->format = func->common->expr->format; + + pathspec = transformExprRecurse(pstate, func->common->pathspec); + + jsexpr->path_spec = + coerce_to_target_type(pstate, pathspec, exprType(pathspec), + JSONPATHOID, -1, + COERCION_EXPLICIT, COERCE_IMPLICIT_CAST, + exprLocation(pathspec)); + if (!jsexpr->path_spec) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("JSON path expression must be type %s, not type %s", + "jsonpath", format_type_be(exprType(pathspec))), + parser_errposition(pstate, exprLocation(pathspec)))); + + /* transform and coerce to json[b] passing arguments */ + transformJsonPassingArgs(pstate, constructName, format, + func->common->passing, + &jsexpr->passing_values, + &jsexpr->passing_names); + + if (func->op != JSON_EXISTS_OP) + jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, + JSON_BEHAVIOR_NULL); + + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + func->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : + JSON_BEHAVIOR_NULL); + + return jsexpr; +} + +/* + * Assign default JSON returning type from the specified format or from + * the context item type. + */ +static void +assignDefaultJsonReturningType(Node *context_item, JsonFormat *context_format, + JsonReturning *ret) +{ + bool is_jsonb; + + ret->format = copyObject(context_format); + + if (ret->format->format_type == JS_FORMAT_DEFAULT) + is_jsonb = exprType(context_item) == JSONBOID; + else + is_jsonb = ret->format->format_type == JS_FORMAT_JSONB; + + ret->typid = is_jsonb ? JSONBOID : JSONOID; + ret->typmod = -1; +} + +/* + * Try to coerce expression to the output type or + * use json_populate_type() for composite, array and domain types or + * use coercion via I/O. + */ +static JsonCoercion * +coerceJsonExpr(ParseState *pstate, Node *expr, const JsonReturning *returning) +{ + char typtype; + JsonCoercion *coercion = makeNode(JsonCoercion); + + coercion->expr = coerceJsonFuncExpr(pstate, expr, returning, false); + + if (coercion->expr) + { + if (coercion->expr == expr) + coercion->expr = NULL; + + return coercion; + } + + typtype = get_typtype(returning->typid); + + if (returning->typid == RECORDOID || + typtype == TYPTYPE_COMPOSITE || + typtype == TYPTYPE_DOMAIN || + type_is_array(returning->typid)) + coercion->via_populate = true; + else + coercion->via_io = true; + + return coercion; +} + +/* + * Transform a JSON output clause of JSON_VALUE and JSON_QUERY. + */ +static void +transformJsonFuncExprOutput(ParseState *pstate, JsonFuncExpr *func, + JsonExpr *jsexpr) +{ + Node *expr = jsexpr->formatted_expr; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + /* JSON_VALUE returns text by default */ + if (func->op == JSON_VALUE_OP && !OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = TEXTOID; + jsexpr->returning->typmod = -1; + } + + if (OidIsValid(jsexpr->returning->typid)) + { + JsonReturning ret; + + if (func->op == JSON_VALUE_OP && + jsexpr->returning->typid != JSONOID && + jsexpr->returning->typid != JSONBOID) + { + /* Forced coercion via I/O for JSON_VALUE for non-JSON types */ + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = NULL; + jsexpr->result_coercion->via_io = true; + return; + } + + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, &ret); + + if (ret.typid != jsexpr->returning->typid || + ret.typmod != jsexpr->returning->typmod) + { + CaseTestExpr *cte = makeNode(CaseTestExpr); + + cte->typeId = exprType(expr); + cte->typeMod = exprTypmod(expr); + cte->collation = exprCollation(expr); + + Assert(cte->typeId == ret.typid); + Assert(cte->typeMod == ret.typmod); + + jsexpr->result_coercion = coerceJsonExpr(pstate, (Node *) cte, + jsexpr->returning); + } + } + else + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, + jsexpr->returning); +} + +/* + * Coerce an expression in JSON DEFAULT behavior to the target output type. + */ +static Node * +coerceDefaultJsonExpr(ParseState *pstate, JsonExpr *jsexpr, Node *defexpr) +{ + int location; + Oid exprtype; + + if (!defexpr) + return NULL; + + exprtype = exprType(defexpr); + location = exprLocation(defexpr); + + if (location < 0) + location = jsexpr->location; + + defexpr = coerce_to_target_type(pstate, + defexpr, + exprtype, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!defexpr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast DEFAULT expression type %s to %s", + format_type_be(exprtype), + format_type_be(jsexpr->returning->typid)), + parser_errposition(pstate, location))); + + return defexpr; +} + +/* + * Initialize SQL/JSON item coercion from the SQL type "typid" to the target + * "returning" type. + */ +static JsonCoercion * +initJsonItemCoercion(ParseState *pstate, Oid typid, + const JsonReturning *returning) +{ + Node *expr; + + if (typid == UNKNOWNOID) + { + expr = (Node *) makeNullConst(UNKNOWNOID, -1, InvalidOid); + } + else + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + + placeholder->typeId = typid; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + expr = (Node *) placeholder; + } + + return coerceJsonExpr(pstate, expr, returning); +} + +static void +initJsonItemCoercions(ParseState *pstate, JsonItemCoercions *coercions, + const JsonReturning *returning, Oid contextItemTypeId) +{ + struct + { + JsonCoercion **coercion; + Oid typid; + } *p, + coercionTypids[] = + { + {&coercions->null, UNKNOWNOID}, + {&coercions->string, TEXTOID}, + {&coercions->numeric, NUMERICOID}, + {&coercions->boolean, BOOLOID}, + {&coercions->date, DATEOID}, + {&coercions->time, TIMEOID}, + {&coercions->timetz, TIMETZOID}, + {&coercions->timestamp, TIMESTAMPOID}, + {&coercions->timestamptz, TIMESTAMPTZOID}, + {&coercions->composite, contextItemTypeId}, + {NULL, InvalidOid} + }; + + for (p = coercionTypids; p->coercion; p++) + *p->coercion = initJsonItemCoercion(pstate, p->typid, returning); +} + +/* + * Transform JSON_VALUE, JSON_QUERY, JSON_EXISTS functions into a JsonExpr node. + */ +static Node * +transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = transformJsonExprCommon(pstate, func); + const char *func_name = NULL; + Node *contextItemExpr = jsexpr->formatted_expr; + + /* + * Disallow FORMAT specification in the RETURNING clause of JSON_EXISTS() + * and JSON_VALUE(). + */ + if (func->output && + (func->op == JSON_VALUE_OP || func->op == JSON_EXISTS_OP)) + { + JsonFormat *format = func->output->returning->format; + + if (format->format_type != JS_FORMAT_DEFAULT || + format->encoding != JS_ENC_DEFAULT) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot specify FORMAT in RETURNING clause of %s", + func->op == JSON_VALUE_OP ? "JSON_VALUE()" : + "JSON_EXISTS()"), + parser_errposition(pstate, format->location))); + } + + switch (func->op) + { + case JSON_VALUE_OP: + func_name = "JSON_VALUE"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->coercions = makeNode(JsonItemCoercions); + initJsonItemCoercions(pstate, jsexpr->coercions, jsexpr->returning, + exprType(contextItemExpr)); + + break; + + case JSON_QUERY_OP: + func_name = "JSON_QUERY"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->wrapper = func->wrapper; + jsexpr->omit_quotes = func->omit_quotes; + + break; + + case JSON_EXISTS_OP: + func_name = "JSON_EXISTS"; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + if (!OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = BOOLOID; + jsexpr->returning->typmod = -1; + } + else if (jsexpr->returning->typid != BOOLOID) + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + int location = exprLocation((Node *) jsexpr); + + placeholder->typeId = BOOLOID; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = + coerce_to_target_type(pstate, (Node *) placeholder, BOOLOID, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!jsexpr->result_coercion->expr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(BOOLOID), + format_type_be(jsexpr->returning->typid)), + parser_coercion_errposition(pstate, location, (Node *) jsexpr))); + + if (jsexpr->result_coercion->expr == (Node *) placeholder) + jsexpr->result_coercion->expr = NULL; + } + break; + } + + if (exprType(contextItemExpr) != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("%s() is not yet implemented for the json type", func_name), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + return (Node *) jsexpr; +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 57247de363..2c83d3a3a9 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1982,6 +1982,21 @@ FigureColnameInternal(Node *node, char **name) /* make JSON_ARRAYAGG act like a regular function */ *name = "json_arrayagg"; return 2; + case T_JsonFuncExpr: + /* make SQL/JSON functions act like a regular function */ + switch (((JsonFuncExpr *) node)->op) + { + case JSON_QUERY_OP: + *name = "json_query"; + return 2; + case JSON_VALUE_OP: + *name = "json_value"; + return 2; + case JSON_EXISTS_OP: + *name = "json_exists"; + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index e27ea8ef97..188b5594e0 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -4426,6 +4426,50 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, } } +/* + * Parses the datetime format string in 'fmt_str' and returns true if it + * contains a timezone specifier, false if not. + */ +bool +datetime_format_has_tz(const char *fmt_str) +{ + bool incache; + int fmt_len = strlen(fmt_str); + int result; + FormatNode *format; + + if (fmt_len > DCH_CACHE_SIZE) + { + /* + * Allocate new memory if format picture is bigger than static cache + * and do not use cache (call parser always) + */ + incache = false; + + format = (FormatNode *) palloc((fmt_len + 1) * sizeof(FormatNode)); + + parse_format(format, fmt_str, DCH_keywords, + DCH_suff, DCH_index, DCH_FLAG, NULL); + } + else + { + /* + * Use cache buffers + */ + DCHCacheEntry *ent = DCH_cache_fetch(fmt_str, false); + + incache = true; + format = ent->format; + } + + result = DCH_datetime_type(format); + + if (!incache) + pfree(format); + + return result & DCH_ZONED; +} + /* * do_to_timestamp: shared code for to_timestamp and to_date * diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index 9781852b0c..ea5b386f8c 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -2162,3 +2162,65 @@ jsonb_float8(PG_FUNCTION_ARGS) PG_RETURN_DATUM(retValue); } + +/* + * Construct an empty array jsonb. + */ +Jsonb * +JsonbMakeEmptyArray(void) +{ + JsonbValue jbv; + + jbv.type = jbvArray; + jbv.val.array.elems = NULL; + jbv.val.array.nElems = 0; + jbv.val.array.rawScalar = false; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Construct an empty object jsonb. + */ +Jsonb * +JsonbMakeEmptyObject(void) +{ + JsonbValue jbv; + + jbv.type = jbvObject; + jbv.val.object.pairs = NULL; + jbv.val.object.nPairs = 0; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Convert jsonb to a C-string stripping quotes from scalar strings. + */ +char * +JsonbUnquote(Jsonb *jb) +{ + if (JB_ROOT_IS_SCALAR(jb)) + { + JsonbValue v; + + (void) JsonbExtractScalar(&jb->root, &v); + + if (v.type == jbvString) + return pnstrdup(v.val.string.val, v.val.string.len); + else if (v.type == jbvBool) + return pstrdup(v.val.boolean ? "true" : "false"); + else if (v.type == jbvNumeric) + return DatumGetCString(DirectFunctionCall1(numeric_out, + PointerGetDatum(v.val.numeric))); + else if (v.type == jbvNull) + return pstrdup("null"); + else + { + elog(ERROR, "unrecognized jsonb value type %d", v.type); + return NULL; + } + } + else + return JsonbToCString(NULL, &jb->root, VARSIZE(jb)); +} diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index a4bfa5e404..2d5fa285fc 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -265,6 +265,7 @@ typedef struct PopulateArrayContext int *dims; /* dimensions */ int *sizes; /* current dimension counters */ int ndims; /* number of dimensions */ + Node *escontext; /* For soft-error capture */ } PopulateArrayContext; /* state for populate_array_json() */ @@ -442,12 +443,13 @@ static void JsValueToJsObject(JsValue *jsv, JsObject *jso); static Datum populate_composite(CompositeIOData *io, Oid typid, const char *colname, MemoryContext mcxt, HeapTupleHeader defaultval, JsValue *jsv, bool isnull); -static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv); +static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext); static void prepare_column_cache(ColumnIOData *column, Oid typid, int32 typmod, MemoryContext mcxt, bool need_scalar); static Datum populate_record_field(ColumnIOData *col, Oid typid, int32 typmod, const char *colname, MemoryContext mcxt, Datum defaultval, - JsValue *jsv, bool *isnull); + JsValue *jsv, bool *isnull, Node *escontext); static RecordIOData *allocate_record_info(MemoryContext mcxt, int ncolumns); static bool JsObjectGetField(JsObject *obj, char *field, JsValue *jsv); static void populate_recordset_record(PopulateRecordsetState *state, JsObject *obj); @@ -459,7 +461,8 @@ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims); static void populate_array_check_dimension(PopulateArrayContext *ctx, int ndim); static void populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv); static Datum populate_array(ArrayIOData *aio, const char *colname, - MemoryContext mcxt, JsValue *jsv); + MemoryContext mcxt, JsValue *jsv, Node *escontext, + bool *isnull); static Datum populate_domain(DomainIOData *io, Oid typid, const char *colname, MemoryContext mcxt, JsValue *jsv, bool isnull); @@ -2484,12 +2487,12 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) if (ndim <= 0) { if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the value of key \"%s\".", ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"))); } @@ -2506,13 +2509,13 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) appendStringInfo(&indices, "[%d]", ctx->sizes[i]); if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s of key \"%s\".", indices.data, ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s.", @@ -2520,7 +2523,11 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) } } -/* set the number of dimensions of the populated array when it becomes known */ +/* + * Set the number of dimensions of the populated array when it becomes known. + * + * Returns without doing anything if the input (ndims) is erratic. + */ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) { @@ -2531,6 +2538,10 @@ populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) if (ndims <= 0) populate_array_report_expected_array(ctx, ndims); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + ctx->ndims = ndims; ctx->dims = palloc(sizeof(int) * ndims); ctx->sizes = palloc0(sizeof(int) * ndims); @@ -2548,12 +2559,16 @@ populate_array_check_dimension(PopulateArrayContext *ctx, int ndim) if (ctx->dims[ndim] == -1) ctx->dims[ndim] = dim; /* assign dimension if not yet known */ else if (ctx->dims[ndim] != dim) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("malformed JSON array"), errdetail("Multidimensional arrays must have " "sub-arrays with matching dimensions."))); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + /* reset the current array dimension size counter */ ctx->sizes[ndim] = 0; @@ -2573,7 +2588,10 @@ populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv) ctx->aio->element_type, ctx->aio->element_typmod, NULL, ctx->mcxt, PointerGetDatum(NULL), - jsv, &element_isnull); + jsv, &element_isnull, ctx->escontext); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; accumArrayResult(ctx->astate, element, element_isnull, ctx->aio->element_type, ctx->acxt); @@ -2594,6 +2612,10 @@ populate_array_object_start(void *_state) else if (ndim < state->ctx->ndims) populate_array_report_expected_array(state->ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2611,6 +2633,10 @@ populate_array_array_end(void *_state) if (ndim < ctx->ndims) populate_array_check_dimension(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2686,6 +2712,10 @@ populate_array_scalar(void *_state, char *token, JsonTokenType tokentype) else if (ndim < ctx->ndims) populate_array_report_expected_array(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + if (ndim == ctx->ndims) { /* remember the scalar element token */ @@ -2715,7 +2745,13 @@ populate_array_json(PopulateArrayContext *ctx, char *json, int len) sem.array_element_end = populate_array_element_end; sem.scalar = populate_array_scalar; - pg_parse_json_or_ereport(state.lex, &sem); + if (!pg_parse_json_or_errsave(state.lex, &sem, ctx->escontext)) + { + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + pfree(state.lex); + return; + } /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2740,10 +2776,15 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ check_stack_depth(); - if (jbv->type != jbvBinary || !JsonContainerIsArray(jbc)) + if (jbv->type != jbvBinary || + !JsonContainerIsArray(jbc) || + JsonContainerIsScalar(jbc)) + { populate_array_report_expected_array(ctx, ndim - 1); - - Assert(!JsonContainerIsScalar(jbc)); + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + return; + } it = JsonbIteratorInit(jbc); @@ -2762,7 +2803,12 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ (tok == WJB_ELEM && (val.type != jbvBinary || !JsonContainerIsArray(val.val.binary.data))))) + { populate_array_assign_ndims(ctx, ndim); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + } jsv.is_json = false; jsv.val.jsonb = &val; @@ -2780,6 +2826,9 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ { /* populate child sub-array */ populate_array_dim_jsonb(ctx, &val, ndim + 1); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2797,12 +2846,18 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ Assert(tok == WJB_DONE && !it); } -/* recursively populate an array from json/jsonb */ +/* + * Recursively populate an array from json/jsonb + * + * *isnull is set to true if an error is reported during parsing. + */ static Datum populate_array(ArrayIOData *aio, const char *colname, MemoryContext mcxt, - JsValue *jsv) + JsValue *jsv, + Node *escontext, + bool *isnull) { PopulateArrayContext ctx; Datum result; @@ -2817,6 +2872,7 @@ populate_array(ArrayIOData *aio, ctx.ndims = 0; /* unknown yet */ ctx.dims = NULL; ctx.sizes = NULL; + ctx.escontext = escontext; if (jsv->is_json) populate_array_json(&ctx, jsv->val.json.str, @@ -2825,7 +2881,16 @@ populate_array(ArrayIOData *aio, else { populate_array_dim_jsonb(&ctx, jsv->val.jsonb, 1); - ctx.dims[0] = ctx.sizes[0]; + /* Nothing to do on an error. */ + if (!SOFT_ERROR_OCCURRED(ctx.escontext)) + ctx.dims[0] = ctx.sizes[0]; + } + + /* Nothing to return if an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx.escontext)) + { + *isnull = true; + return (Datum) 0; } Assert(ctx.ndims > 0); @@ -2842,6 +2907,7 @@ populate_array(ArrayIOData *aio, pfree(ctx.sizes); pfree(lbs); + *isnull = false; return result; } @@ -2957,7 +3023,8 @@ populate_composite(CompositeIOData *io, /* populate non-null scalar value from json/jsonb value */ static Datum -populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) +populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext) { Datum res; char *str = NULL; @@ -3028,7 +3095,11 @@ populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) elog(ERROR, "unrecognized jsonb type: %d", (int) jbv->type); } - res = InputFunctionCall(&io->typiofunc, str, io->typioparam, typmod); + if (!InputFunctionCallSafe(&io->typiofunc, str, io->typioparam, typmod, + escontext, &res)) + { + res = (Datum) 0; + } /* free temporary buffer */ if (str != json) @@ -3054,7 +3125,7 @@ populate_domain(DomainIOData *io, res = populate_record_field(io->base_io, io->base_typid, io->base_typmod, colname, mcxt, PointerGetDatum(NULL), - jsv, &isnull); + jsv, &isnull, NULL); Assert(!isnull); } @@ -3159,7 +3230,8 @@ populate_record_field(ColumnIOData *col, MemoryContext mcxt, Datum defaultval, JsValue *jsv, - bool *isnull) + bool *isnull, + Node *escontext) { TypeCat typcat; @@ -3192,10 +3264,12 @@ populate_record_field(ColumnIOData *col, switch (typcat) { case TYPECAT_SCALAR: - return populate_scalar(&col->scalar_io, typid, typmod, jsv); + return populate_scalar(&col->scalar_io, typid, typmod, jsv, + escontext); case TYPECAT_ARRAY: - return populate_array(&col->io.array, colname, mcxt, jsv); + return populate_array(&col->io.array, colname, mcxt, jsv, + escontext, isnull); case TYPECAT_COMPOSITE: case TYPECAT_COMPOSITE_DOMAIN: @@ -3216,6 +3290,53 @@ populate_record_field(ColumnIOData *col, } } +/* recursively populate specified type from a json/jsonb value */ +Datum +json_populate_type(Datum json_val, Oid json_type, Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext) +{ + JsValue jsv = {0}; + JsonbValue jbv; + + jsv.is_json = json_type == JSONOID; + + if (*isnull) + { + if (jsv.is_json) + jsv.val.json.str = NULL; + else + jsv.val.jsonb = NULL; + } + else if (jsv.is_json) + { + text *json = DatumGetTextPP(json_val); + + jsv.val.json.str = VARDATA_ANY(json); + jsv.val.json.len = VARSIZE_ANY_EXHDR(json); + jsv.val.json.type = JSON_TOKEN_INVALID; /* not used in + * populate_composite() */ + } + else + { + Jsonb *jsonb = DatumGetJsonbP(json_val); + + jsv.val.jsonb = &jbv; + + /* fill binary jsonb value pointing to jb */ + jbv.type = jbvBinary; + jbv.val.binary.data = &jsonb->root; + jbv.val.binary.len = VARSIZE(jsonb) - VARHDRSZ; + } + + if (!*cache) + *cache = MemoryContextAllocZero(mcxt, sizeof(ColumnIOData)); + + return populate_record_field(*cache, typid, typmod, NULL, mcxt, + PointerGetDatum(NULL), &jsv, isnull, + escontext); +} + static RecordIOData * allocate_record_info(MemoryContext mcxt, int ncolumns) { @@ -3357,7 +3478,8 @@ populate_record(TupleDesc tupdesc, mcxt, nulls[i] ? (Datum) 0 : values[i], &field, - &nulls[i]); + &nulls[i], + NULL); } res = heap_form_tuple(tupdesc, values, nulls); diff --git a/src/backend/utils/adt/jsonpath.c b/src/backend/utils/adt/jsonpath.c index 7891fde310..5194d0d91f 100644 --- a/src/backend/utils/adt/jsonpath.c +++ b/src/backend/utils/adt/jsonpath.c @@ -68,7 +68,9 @@ #include "libpq/pqformat.h" #include "nodes/miscnodes.h" #include "miscadmin.h" +#include "nodes/nodeFuncs.h" #include "utils/builtins.h" +#include "utils/formatting.h" #include "utils/json.h" #include "utils/jsonpath.h" @@ -1109,3 +1111,256 @@ jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, return true; } + +/* SQL/JSON datatype status: */ +typedef enum JsonPathDatatypeStatus +{ + jpdsNonDateTime, /* null, bool, numeric, string, array, object */ + jpdsUnknownDateTime, /* unknown datetime type */ + jpdsDateTimeZoned, /* timetz, timestamptz */ + jpdsDateTimeNonZoned /* time, timestamp, date */ +} JsonPathDatatypeStatus; + +/* Context for jspIsMutableWalker() */ +typedef struct JsonPathMutableContext +{ + List *varnames; /* list of variable names */ + List *varexprs; /* list of variable expressions */ + JsonPathDatatypeStatus current; /* status of @ item */ + bool lax; /* jsonpath is lax or strict */ + bool mutable; /* resulting mutability status */ +} JsonPathMutableContext; + +/* + * Recursive walker for jspIsMutable() + */ +static JsonPathDatatypeStatus +jspIsMutableWalker(JsonPathItem *jpi, JsonPathMutableContext *cxt) +{ + JsonPathItem next; + JsonPathDatatypeStatus status = jpdsNonDateTime; + + while (!cxt->mutable) + { + JsonPathItem arg; + JsonPathDatatypeStatus leftStatus; + JsonPathDatatypeStatus rightStatus; + + switch (jpi->type) + { + case jpiRoot: + Assert(status == jpdsNonDateTime); + break; + + case jpiCurrent: + Assert(status == jpdsNonDateTime); + status = cxt->current; + break; + + case jpiFilter: + { + JsonPathDatatypeStatus prevStatus = cxt->current; + + cxt->current = status; + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + + cxt->current = prevStatus; + break; + } + + case jpiVariable: + { + int32 len; + const char *name = jspGetString(jpi, &len); + ListCell *lc1; + ListCell *lc2; + + Assert(status == jpdsNonDateTime); + + forboth(lc1, cxt->varnames, lc2, cxt->varexprs) + { + String *varname = lfirst_node(String, lc1); + Node *varexpr = lfirst(lc2); + + if (strncmp(varname->sval, name, len)) + continue; + + switch (exprType(varexpr)) + { + case DATEOID: + case TIMEOID: + case TIMESTAMPOID: + status = jpdsDateTimeNonZoned; + break; + + case TIMETZOID: + case TIMESTAMPTZOID: + status = jpdsDateTimeZoned; + break; + + default: + status = jpdsNonDateTime; + break; + } + + break; + } + break; + } + + case jpiEqual: + case jpiNotEqual: + case jpiLess: + case jpiGreater: + case jpiLessOrEqual: + case jpiGreaterOrEqual: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + leftStatus = jspIsMutableWalker(&arg, cxt); + + jspGetRightArg(jpi, &arg); + rightStatus = jspIsMutableWalker(&arg, cxt); + + /* + * Comparison of datetime type with different timezone status + * is mutable. + */ + if (leftStatus != jpdsNonDateTime && + rightStatus != jpdsNonDateTime && + (leftStatus == jpdsUnknownDateTime || + rightStatus == jpdsUnknownDateTime || + leftStatus != rightStatus)) + cxt->mutable = true; + break; + + case jpiNot: + case jpiIsUnknown: + case jpiExists: + case jpiPlus: + case jpiMinus: + Assert(status == jpdsNonDateTime); + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiAnd: + case jpiOr: + case jpiAdd: + case jpiSub: + case jpiMul: + case jpiDiv: + case jpiMod: + case jpiStartsWith: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + jspGetRightArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiIndexArray: + for (int i = 0; i < jpi->content.array.nelems; i++) + { + JsonPathItem from; + JsonPathItem to; + + if (jspGetArraySubscript(jpi, &from, &to, i)) + jspIsMutableWalker(&to, cxt); + + jspIsMutableWalker(&from, cxt); + } + /* FALLTHROUGH */ + + case jpiAnyArray: + if (!cxt->lax) + status = jpdsNonDateTime; + break; + + case jpiAny: + if (jpi->content.anybounds.first > 0) + status = jpdsNonDateTime; + break; + + case jpiDatetime: + if (jpi->content.arg) + { + char *template; + + jspGetArg(jpi, &arg); + if (arg.type != jpiString) + { + status = jpdsNonDateTime; + break; /* there will be runtime error */ + } + + template = jspGetString(&arg, NULL); + if (datetime_format_has_tz(template)) + status = jpdsDateTimeZoned; + else + status = jpdsDateTimeNonZoned; + } + else + { + status = jpdsUnknownDateTime; + } + break; + + case jpiLikeRegex: + Assert(status == jpdsNonDateTime); + jspInitByBuffer(&arg, jpi->base, jpi->content.like_regex.expr); + jspIsMutableWalker(&arg, cxt); + break; + + /* literals */ + case jpiNull: + case jpiString: + case jpiNumeric: + case jpiBool: + /* accessors */ + case jpiKey: + case jpiAnyKey: + /* special items */ + case jpiSubscript: + case jpiLast: + /* item methods */ + case jpiType: + case jpiSize: + case jpiAbs: + case jpiFloor: + case jpiCeiling: + case jpiDouble: + case jpiKeyValue: + status = jpdsNonDateTime; + break; + } + + if (!jspGetNext(jpi, &next)) + break; + + jpi = &next; + } + + return status; +} + +/* + * Check whether jsonpath expression is immutable or not. + */ +bool +jspIsMutable(JsonPath *path, List *varnames, List *varexprs) +{ + JsonPathMutableContext cxt; + JsonPathItem jpi; + + cxt.varnames = varnames; + cxt.varexprs = varexprs; + cxt.current = jpdsNonDateTime; + cxt.lax = (path->header & JSONPATH_LAX) != 0; + cxt.mutable = false; + + jspInit(&jpi, path); + jspIsMutableWalker(&jpi, &cxt); + + return cxt.mutable; +} diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index 2d0599b4aa..eded22e194 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -87,12 +87,16 @@ typedef struct JsonBaseObjectInfo int id; } JsonBaseObjectInfo; +typedef int (*JsonPathVarCallback) (void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); + /* * Context of jsonpath execution. */ typedef struct JsonPathExecContext { - Jsonb *vars; /* variables to substitute into jsonpath */ + void *vars; /* variables to substitute into jsonpath */ + JsonPathVarCallback getVar; JsonbValue *root; /* for $ evaluation */ JsonbValue *current; /* for @ evaluation */ JsonBaseObjectInfo baseObject; /* "base object" for .keyvalue() @@ -174,7 +178,8 @@ typedef JsonPathBool (*JsonPathPredicateCallback) (JsonPathItem *jsp, void *param); typedef Numeric (*BinaryArithmFunc) (Numeric num1, Numeric num2, bool *error); -static JsonPathExecResult executeJsonPath(JsonPath *path, Jsonb *vars, +static JsonPathExecResult executeJsonPath(JsonPath *path, void *vars, + JsonPathVarCallback getVar, Jsonb *json, bool throwErrors, JsonValueList *result, bool useTz); static JsonPathExecResult executeItem(JsonPathExecContext *cxt, @@ -225,8 +230,13 @@ static JsonPathExecResult appendBoolResult(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonValueList *found, JsonPathBool res); static void getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, JsonbValue *value); +static int GetJsonPathVar(void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); static void getJsonPathVariable(JsonPathExecContext *cxt, - JsonPathItem *variable, Jsonb *vars, JsonbValue *value); + JsonPathItem *variable, JsonbValue *value); +static int getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, + int varNameLen, JsonbValue *val, + JsonbValue *baseObject); static int JsonbArraySize(JsonbValue *jb); static JsonPathBool executeComparison(JsonPathItem *cmp, JsonbValue *lv, JsonbValue *rv, void *p); @@ -284,7 +294,8 @@ jsonb_path_exists_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - res = executeJsonPath(jp, vars, jb, !silent, NULL, tz); + res = executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, NULL, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -339,7 +350,8 @@ jsonb_path_match_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -417,7 +429,8 @@ jsonb_path_query_internal(FunctionCallInfo fcinfo, bool tz) vars = PG_GETARG_JSONB_P_COPY(2); silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); funcctx->user_fctx = JsonValueListGetList(&found); @@ -464,7 +477,8 @@ jsonb_path_query_array_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_RETURN_JSONB_P(JsonbValueToJsonb(wrapItemsInArray(&found))); } @@ -495,7 +509,8 @@ jsonb_path_query_first_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); if (JsonValueListLength(&found) >= 1) PG_RETURN_JSONB_P(JsonbValueToJsonb(JsonValueListHead(&found))); @@ -537,8 +552,9 @@ jsonb_path_query_first_tz(PG_FUNCTION_ARGS) * In other case it tries to find all the satisfied result items. */ static JsonPathExecResult -executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, - JsonValueList *result, bool useTz) +executeJsonPath(JsonPath *path, void *vars, JsonPathVarCallback getVar, + Jsonb *json, bool throwErrors, JsonValueList *result, + bool useTz) { JsonPathExecContext cxt; JsonPathExecResult res; @@ -550,22 +566,16 @@ executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, if (!JsonbExtractScalar(&json->root, &jbv)) JsonbInitBinary(&jbv, json); - if (vars && !JsonContainerIsObject(&vars->root)) - { - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"vars\" argument is not an object"), - errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); - } - cxt.vars = vars; + cxt.getVar = getVar; cxt.laxMode = (path->header & JSONPATH_LAX) != 0; cxt.ignoreStructuralErrors = cxt.laxMode; cxt.root = &jbv; cxt.current = &jbv; cxt.baseObject.jbc = NULL; cxt.baseObject.id = 0; - cxt.lastGeneratedObjectId = vars ? 2 : 1; + /* 1 + number of base objects in vars */ + cxt.lastGeneratedObjectId = 1 + getVar(vars, NULL, 0, NULL, NULL); cxt.innermostArraySize = -1; cxt.throwErrors = throwErrors; cxt.useTz = useTz; @@ -2108,54 +2118,118 @@ getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, &value->val.string.len); break; case jpiVariable: - getJsonPathVariable(cxt, item, cxt->vars, value); + getJsonPathVariable(cxt, item, value); return; default: elog(ERROR, "unexpected jsonpath item type"); } } +/* + * Returns the computed value of a JSON path variable with given name. + */ +static int +GetJsonPathVar(void *cxt, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject) +{ + JsonPathVariable *var = NULL; + List *vars = cxt; + ListCell *lc; + int id = 1; + + if (!varName) + return list_length(vars); + + foreach(lc, vars) + { + JsonPathVariable *curvar = lfirst(lc); + + if (!strncmp(curvar->name, varName, varNameLen)) + { + var = curvar; + break; + } + + id++; + } + + if (!var) + return -1; + + if (var->isnull) + { + val->type = jbvNull; + return 0; + } + + JsonItemFromDatum(var->value, var->typid, var->typmod, val); + + *baseObject = *val; + return id; +} + /* * Get the value of variable passed to jsonpath executor */ static void getJsonPathVariable(JsonPathExecContext *cxt, JsonPathItem *variable, - Jsonb *vars, JsonbValue *value) + JsonbValue *value) { char *varName; int varNameLength; + JsonbValue baseObject; + int baseObjectId; + + Assert(variable->type == jpiVariable); + varName = jspGetString(variable, &varNameLength); + + if (!cxt->vars || + (baseObjectId = cxt->getVar(cxt->vars, varName, varNameLength, value, + &baseObject)) < 0) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("could not find jsonpath variable \"%s\"", + pnstrdup(varName, varNameLength)))); + + if (baseObjectId > 0) + setBaseObject(cxt, &baseObject, baseObjectId); +} + +static int +getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, int varNameLength, + JsonbValue *value, JsonbValue *baseObject) +{ + Jsonb *vars = varsJsonb; JsonbValue tmp; JsonbValue *v; - if (!vars) + if (!varName) { - value->type = jbvNull; - return; + if (vars && !JsonContainerIsObject(&vars->root)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"vars\" argument is not an object"), + errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); + } + + return vars ? 1 : 0; /* count of base objects */ } - Assert(variable->type == jpiVariable); - varName = jspGetString(variable, &varNameLength); tmp.type = jbvString; tmp.val.string.val = varName; tmp.val.string.len = varNameLength; v = findJsonbValueFromContainer(&vars->root, JB_FOBJECT, &tmp); - if (v) - { - *value = *v; - pfree(v); - } - else - { - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("could not find jsonpath variable \"%s\"", - pnstrdup(varName, varNameLength)))); - } + if (!v) + return -1; - JsonbInitBinary(&tmp, vars); - setBaseObject(cxt, &tmp, 1); + *value = *v; + pfree(v); + + JsonbInitBinary(baseObject, vars); + return 1; } /**************** Support functions for JsonPath execution *****************/ @@ -2812,3 +2886,240 @@ compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, return DatumGetInt32(DirectFunctionCall2(cmpfunc, val1, val2)); } + +/* Executor-callable JSON_EXISTS implementation */ +bool +JsonPathExists(Datum jb, JsonPath *jp, List *vars, bool *error) +{ + JsonPathExecResult res = executeJsonPath(jp, vars, GetJsonPathVar, + DatumGetJsonbP(jb), !error, NULL, + true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + *error = true; + + return res == jperOk; +} + +/* Executor-callable JSON_QUERY implementation */ +Datum +JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, bool *empty, + bool *error, List *vars) +{ + JsonbValue *first; + bool wrap; + JsonValueList found = {0}; + JsonPathExecResult res PG_USED_FOR_ASSERTS_ONLY; + int count; + + res = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + { + *error = true; + *empty = false; + return (Datum) 0; + } + + count = JsonValueListLength(&found); + + first = count ? JsonValueListHead(&found) : NULL; + + if (!first) + wrap = false; + else if (wrapper == JSW_NONE) + wrap = false; + else if (wrapper == JSW_UNCONDITIONAL) + wrap = true; + else if (wrapper == JSW_CONDITIONAL) + wrap = count > 1 || + IsAJsonbScalar(first) || + (first->type == jbvBinary && + JsonContainerIsScalar(first->val.binary.data)); + else + { + elog(ERROR, "unrecognized json wrapper %d", wrapper); + wrap = false; + } + + if (wrap) + return JsonbPGetDatum(JsonbValueToJsonb(wrapItemsInArray(&found))); + + if (count > 1) + { + if (error) + { + *error = true; + return (Datum) 0; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_QUERY should return singleton item without wrapper"), + errhint("Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array."))); + } + + if (first) + return JsonbPGetDatum(JsonbValueToJsonb(first)); + + *empty = true; + return PointerGetDatum(NULL); +} + +/* Executor-callable JSON_VALUE implementation */ +JsonbValue * +JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars) +{ + JsonbValue *res; + JsonValueList found = {0}; + JsonPathExecResult jper PG_USED_FOR_ASSERTS_ONLY; + int count; + + jper = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(jper)); + + if (error && jperIsError(jper)) + { + *error = true; + *empty = false; + return NULL; + } + + count = JsonValueListLength(&found); + + *empty = !count; + + if (*empty) + return NULL; + + if (count > 1) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + res = JsonValueListHead(&found); + + if (res->type == jbvBinary && + JsonContainerIsScalar(res->val.binary.data)) + JsonbExtractScalar(res->val.binary.data, res); + + if (!IsAJsonbScalar(res)) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_SCALAR_REQUIRED), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + if (res->type == jbvNull) + return NULL; + + return res; +} + +static void +JsonbValueInitNumericDatum(JsonbValue *jbv, Datum num) +{ + jbv->type = jbvNumeric; + jbv->val.numeric = DatumGetNumeric(num); +} + +void +JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) +{ + switch (typid) + { + case BOOLOID: + res->type = jbvBool; + res->val.boolean = DatumGetBool(val); + break; + case NUMERICOID: + JsonbValueInitNumericDatum(res, val); + break; + case INT2OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int2_numeric, val)); + break; + case INT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int4_numeric, val)); + break; + case INT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int8_numeric, val)); + break; + case FLOAT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float4_numeric, val)); + break; + case FLOAT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float8_numeric, val)); + break; + case TEXTOID: + case VARCHAROID: + res->type = jbvString; + res->val.string.val = VARDATA_ANY(val); + res->val.string.len = VARSIZE_ANY_EXHDR(val); + break; + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + res->type = jbvDatetime; + res->val.datetime.value = val; + res->val.datetime.typid = typid; + res->val.datetime.typmod = typmod; + res->val.datetime.tz = 0; + break; + case JSONBOID: + { + JsonbValue *jbv = res; + Jsonb *jb = DatumGetJsonbP(val); + + if (JsonContainerIsScalar(&jb->root)) + { + bool result PG_USED_FOR_ASSERTS_ONLY; + + result = JsonbExtractScalar(&jb->root, jbv); + Assert(result); + } + else + JsonbInitBinary(jbv, jb); + break; + } + case JSONOID: + { + text *txt = DatumGetTextP(val); + char *str = text_to_cstring(txt); + Jsonb *jb; + + jb = DatumGetJsonbP(DirectFunctionCall1(jsonb_in, + CStringGetDatum(str))); + pfree(str); + + JsonItemFromDatum(JsonbPGetDatum(jb), JSONBOID, -1, res); + break; + } + default: + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); + } +} diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 03f2835c3f..d4ebeb18dd 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -476,6 +476,8 @@ static void get_const_expr(Const *constval, deparse_context *context, int showtype); static void get_const_collation(Const *constval, deparse_context *context); static void get_json_format(JsonFormat *format, StringInfo buf); +static void get_json_returning(JsonReturning *returning, StringInfo buf, + bool json_format_by_default); static void get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, bool showimplicit); static void get_json_constructor_options(JsonConstructorExpr *ctor, @@ -518,6 +520,8 @@ static char *generate_qualified_type_name(Oid typid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); +static void get_json_path_spec(Node *path_spec, deparse_context *context, + bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8279,6 +8283,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_WindowFunc: case T_FuncExpr: case T_JsonConstructorExpr: + case T_JsonExpr: /* function-like: name(..) or name[..] */ return true; @@ -8450,6 +8455,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_GroupingFunc: /* own parentheses */ case T_WindowFunc: /* own parentheses */ case T_CaseExpr: /* other separators */ + case T_JsonExpr: /* own parentheses */ return true; default: return false; @@ -8565,6 +8571,65 @@ get_rule_expr_paren(Node *node, deparse_context *context, appendStringInfoChar(context->buf, ')'); } +static void +get_json_behavior(JsonBehavior *behavior, deparse_context *context, + const char *on) +{ + /* + * The order of array elements must correspond to the order of + * JsonBehaviorType members. + */ + const char *behavior_names[] = + { + " NULL", + " ERROR", + " EMPTY", + " TRUE", + " FALSE", + " UNKNOWN", + " EMPTY ARRAY", + " EMPTY OBJECT", + " DEFAULT " + }; + + if ((int) behavior->btype < 0 || behavior->btype >= lengthof(behavior_names)) + elog(ERROR, "invalid json behavior type: %d", behavior->btype); + + appendStringInfoString(context->buf, behavior_names[behavior->btype]); + + if (behavior->btype == JSON_BEHAVIOR_DEFAULT) + get_rule_expr(behavior->default_expr, context, false); + + appendStringInfo(context->buf, " ON %s", on); +} + +/* + * get_json_expr_options + * + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + */ +static void +get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, + JsonBehaviorType default_behavior) +{ + if (jsexpr->op == JSON_QUERY_OP) + { + if (jsexpr->wrapper == JSW_CONDITIONAL) + appendStringInfo(context->buf, " WITH CONDITIONAL WRAPPER"); + else if (jsexpr->wrapper == JSW_UNCONDITIONAL) + appendStringInfo(context->buf, " WITH UNCONDITIONAL WRAPPER"); + + if (jsexpr->omit_quotes) + appendStringInfo(context->buf, " OMIT QUOTES"); + } + + if (jsexpr->op != JSON_EXISTS_OP && + jsexpr->on_empty->btype != default_behavior) + get_json_behavior(jsexpr->on_empty, context, "EMPTY"); + + if (jsexpr->on_error->btype != default_behavior) + get_json_behavior(jsexpr->on_error, context, "ERROR"); +} /* ---------- * get_rule_expr - Parse back an expression @@ -9724,6 +9789,7 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonValueExpr: { JsonValueExpr *jve = (JsonValueExpr *) node; @@ -9773,6 +9839,63 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + switch (jexpr->op) + { + case JSON_QUERY_OP: + appendStringInfoString(buf, "JSON_QUERY("); + break; + case JSON_VALUE_OP: + appendStringInfoString(buf, "JSON_VALUE("); + break; + case JSON_EXISTS_OP: + appendStringInfoString(buf, "JSON_EXISTS("); + break; + } + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_json_path_spec(jexpr->path_spec, context, showimplicit); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoString(buf, " PASSING "); + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + get_rule_expr((Node *) lfirst(lc2), context, showimplicit); + appendStringInfo(buf, " AS %s", + ((String *) lfirst_node(String, lc1))->sval); + } + } + + if (jexpr->op != JSON_EXISTS_OP || + jexpr->returning->typid != BOOLOID) + get_json_returning(jexpr->returning, context->buf, + jexpr->op == JSON_QUERY_OP); + + get_json_expr_options(jexpr, context, + jexpr->op == JSON_EXISTS_OP ? + JSON_BEHAVIOR_FALSE : JSON_BEHAVIOR_NULL); + + appendStringInfoString(buf, ")"); + } + break; + case T_List: { char *sep; @@ -9896,6 +10019,7 @@ looks_like_function(Node *node) case T_MinMaxExpr: case T_SQLValueFunction: case T_XmlExpr: + case T_JsonExpr: /* these are all accepted by func_expr_common_subexpr */ return true; default: @@ -10755,6 +10879,18 @@ get_const_collation(Const *constval, deparse_context *context) } } +/* + * get_json_path_spec - Parse back a JSON path specification + */ +static void +get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit) +{ + if (IsA(path_spec, Const)) + get_const_expr((Const *) path_spec, context, -1); + else + get_rule_expr(path_spec, context, showimplicit); +} + /* * get_json_format - Parse back a JsonFormat node */ diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index 048573c2bc..eefda99bc3 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -16,12 +16,15 @@ #include "executor/nodeAgg.h" #include "nodes/execnodes.h" +#include "nodes/miscnodes.h" /* forward references to avoid circularity */ struct ExprEvalStep; struct SubscriptingRefState; struct ScalarArrayOpExprHashTable; struct JsonConstructorExprState; +struct JsonbValue; +struct JsonExprState; /* Bits in ExprState->flags (see also execnodes.h for public flag bits): */ /* expression's interpreter has been initialized */ @@ -238,6 +241,11 @@ typedef enum ExprEvalOp EEOP_XMLEXPR, EEOP_JSON_CONSTRUCTOR, EEOP_IS_JSON, + EEOP_JSONEXPR_SKIP, + EEOP_JSONEXPR_PATH, + EEOP_JSONEXPR_BEHAVIOR, + EEOP_JSONEXPR_COERCION, + EEOP_JSONEXPR_COERCION_FINISH, EEOP_AGGREF, EEOP_GROUPING_FUNC, EEOP_WINDOW_FUNC, @@ -689,6 +697,57 @@ typedef struct ExprEvalStep JsonIsPredicate *pred; /* original expression node */ } is_json; + /* for EEOP_JSONEXPR_PATH */ + struct + { + struct JsonExprState *jsestate; + } jsonexpr; + + /* for EEOP_JSONEXPR_SKIP */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprSkip() */ + int jump_coercion; + int jump_passing_args; + } jsonexpr_skip; + + /* for EEOP_JSONEXPR_BEHAVIOR */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprBehavior() */ + int jump_onerror_expr; + int jump_onempty_expr; + int jump_coercion; + int jump_skip_coercion; + } jsonexpr_behavior; + + /* for EEOP_JSONEXPR_COERCION */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion; + + /* for EEOP_JSONEXPR_COERCION_FINISH */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion_finish; } d; } ExprEvalStep; @@ -752,6 +811,111 @@ typedef struct JsonConstructorExprState int nargs; } JsonConstructorExprState; +/* + * Information computed before evaluating a JsonExpr expression. + */ +typedef struct JsonExprPreEvalState +{ + /* value/isnull for JsonExpr.formatted_expr */ + NullableDatum formatted_expr; + + /* value/isnull for JsonExpr.pathspec */ + NullableDatum pathspec; + + /* JsonPathVariable entries for JsonExpr.passing_values */ + List *args; +} JsonExprPreEvalState; + +/* + * State for a given member of JsonItemCoercions. + */ +typedef struct JsonCoercionState +{ + /* Expression used to evaluate the coercion */ + JsonCoercion *coercion; + + /* ExprEvalStep to compute this coercion's expression */ + int jump_eval_expr; +} JsonCoercionState; + +/* + * State for evaluating the coercion for a given JSON item using one of + * the following coercions. + * + * Note that while ExecInitExprRec() for JsonItemCoercions will initialize + * ExprEvalSteps for all of the members that need it, only one will get run + * during a given evaluation of the enclosing JsonExpr depending on the type + * of the result JSON item. + */ +typedef struct JsonItemCoercionsState +{ + JsonCoercionState *null; + JsonCoercionState *string; + JsonCoercionState *numeric; + JsonCoercionState *boolean; + JsonCoercionState *date; + JsonCoercionState *time; + JsonCoercionState *timetz; + JsonCoercionState *timestamp; + JsonCoercionState *timestamptz; + JsonCoercionState *composite; +} JsonItemCoercionsState; + +/* + * State for some post-JsonExpr-evaluation processing steps that gets filled + * in JsonExpr evaluation. + */ +typedef struct JsonExprPostEvalState +{ + /* Is JSON item empty? */ + bool empty; + + /* Did JSON item evaluation cause an error? */ + bool error; + + /* Cache for json_populate_type() called for coercion in some cases */ + void *cache; + + /* + * State for evaluating a JSON item coercion. Points to one of those in + * JsonExprState.coercions chosen by ExecPrepareJsonItemCoercion(). + */ + JsonCoercionState *item_jcstate; + bool coercion_error; + bool coercion_done; + + ErrorSaveContext escontext; +} JsonExprPostEvalState; + +/* EEOP_JSONEXPR state, too big to inline */ +typedef struct JsonExprState +{ + /* original expression node */ + JsonExpr *jsexpr; + + /* + * Should errors be thrown or handled softly? Different parts of + * evaluating a given JsonExpr may require different treatment depending + * on the JsonExprOp; see ExecEvalJsonExprBehavior(). + */ + bool throw_error; + + JsonExprPreEvalState pre_eval; + JsonExprPostEvalState post_eval; + + struct + { + FmgrInfo *finfo; /* typinput function for output type */ + Oid typioparam; + } input; /* I/O info for output type */ + + /* + * Either of the following two is used by ExecEvalJsonExprCoercion() to + * apply coercion to the final result if needed. + */ + JsonCoercionState *result_jcstate; + JsonItemCoercionsState *item_jcstates; +} JsonExprState; /* functions in execExpr.c */ extern void ExprEvalPushStep(ExprState *es, const ExprEvalStep *s); @@ -805,6 +969,14 @@ extern void ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op); extern void ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull); +extern int ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op); +extern void ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, + ExprContext *econtext); extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op); extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op, ExprContext *econtext); diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index c677e490d7..709ad967ba 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -17,6 +17,7 @@ #include "executor/execdesc.h" #include "fmgr.h" #include "nodes/lockoptions.h" +#include "nodes/miscnodes.h" #include "nodes/parsenodes.h" #include "utils/memutils.h" diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index cb714f4a19..584bf7001a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -129,6 +129,9 @@ typedef struct ExprState Datum *innermost_domainval; bool *innermost_domainnull; + + /* ErrorSaveContext for soft-error capture. */ + Node *escontext; } ExprState; diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 3180703005..c1d437b44e 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -112,6 +112,7 @@ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *raw_expr, Expr *formatted_expr, JsonFormat *format); +extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index d926713bd9..7d63a37d5f 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1727,6 +1727,12 @@ typedef enum JsonQuotes JS_QUOTES_OMIT /* OMIT QUOTES */ } JsonQuotes; +/* + * JsonPathSpec - + * representation of JSON path constant + */ +typedef char *JsonPathSpec; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1738,6 +1744,48 @@ typedef struct JsonOutput JsonReturning *returning; /* RETURNING FORMAT clause and type Oids */ } JsonOutput; +/* + * JsonArgument - + * representation of argument from JSON PASSING clause + */ +typedef struct JsonArgument +{ + NodeTag type; + JsonValueExpr *val; /* argument value expression */ + char *name; /* argument name */ +} JsonArgument; + +/* + * JsonCommon - + * representation of common syntax of functions using JSON path + */ +typedef struct JsonCommon +{ + NodeTag type; + JsonValueExpr *expr; /* context item expression */ + Node *pathspec; /* JSON path specification expression */ + char *pathname; /* path name, if any */ + List *passing; /* list of PASSING clause arguments, if any */ + int location; /* token location, or -1 if unknown */ +} JsonCommon; + +/* + * JsonFuncExpr - + * untransformed representation of JSON function expressions + */ +typedef struct JsonFuncExpr +{ + NodeTag type; + JsonExprOp op; /* expression type */ + JsonCommon *common; /* common syntax */ + JsonOutput *output; /* output clause, if specified */ + JsonBehavior *on_empty; /* ON EMPTY behavior, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + JsonWrapper wrapper; /* array wrapper behavior (JSON_QUERY only) */ + bool omit_quotes; /* omit or keep quotes? (JSON_QUERY only) */ + int location; /* token location, or -1 if unknown */ +} JsonFuncExpr; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 4104371bd9..64fdbc57fa 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1544,6 +1544,17 @@ typedef struct XmlExpr int location; } XmlExpr; +/* + * JsonExprOp - + * enumeration of JSON functions using JSON path + */ +typedef enum JsonExprOp +{ + JSON_VALUE_OP, /* JSON_VALUE() */ + JSON_QUERY_OP, /* JSON_QUERY() */ + JSON_EXISTS_OP /* JSON_EXISTS() */ +} JsonExprOp; + /* * JsonEncoding - * representation of JSON ENCODING clause @@ -1568,6 +1579,37 @@ typedef enum JsonFormatType * jsonb */ } JsonFormatType; +/* + * JsonBehaviorType - + * enumeration of behavior types used in JSON ON ... BEHAVIOR clause + * + * If enum members are reordered, get_json_behavior() from ruleutils.c + * must be updated accordingly. + */ +typedef enum JsonBehaviorType +{ + JSON_BEHAVIOR_NULL = 0, + JSON_BEHAVIOR_ERROR, + JSON_BEHAVIOR_EMPTY, + JSON_BEHAVIOR_TRUE, + JSON_BEHAVIOR_FALSE, + JSON_BEHAVIOR_UNKNOWN, + JSON_BEHAVIOR_EMPTY_ARRAY, + JSON_BEHAVIOR_EMPTY_OBJECT, + JSON_BEHAVIOR_DEFAULT +} JsonBehaviorType; + +/* + * JsonWrapper - + * representation of WRAPPER clause for JSON_QUERY() + */ +typedef enum JsonWrapper +{ + JSW_NONE, + JSW_CONDITIONAL, + JSW_UNCONDITIONAL, +} JsonWrapper; + /* * JsonFormat - * representation of JSON FORMAT clause @@ -1662,6 +1704,73 @@ typedef struct JsonIsPredicate int location; /* token location, or -1 if unknown */ } JsonIsPredicate; +/* + * JsonBehavior - + * representation of JSON ON ... BEHAVIOR clause + */ +typedef struct JsonBehavior +{ + NodeTag type; + JsonBehaviorType btype; /* behavior type */ + Node *default_expr; /* default expression, if any */ +} JsonBehavior; + +/* + * JsonCoercion - + * coercion from SQL/JSON item types to SQL types + */ +typedef struct JsonCoercion +{ + NodeTag type; + Node *expr; /* resulting expression coerced to target type */ + bool via_populate; /* coerce result using json_populate_type()? */ + bool via_io; /* coerce result using type input function? */ + Oid collation; /* collation for coercion via I/O or populate */ +} JsonCoercion; + +/* + * JsonItemCoercions - + * expressions for coercion from SQL/JSON item types directly to the + * output SQL type + */ +typedef struct JsonItemCoercions +{ + NodeTag type; + JsonCoercion *null; + JsonCoercion *string; + JsonCoercion *numeric; + JsonCoercion *boolean; + JsonCoercion *date; + JsonCoercion *time; + JsonCoercion *timetz; + JsonCoercion *timestamp; + JsonCoercion *timestamptz; + JsonCoercion *composite; /* arrays and objects */ +} JsonItemCoercions; + +/* + * JsonExpr - + * transformed representation of JSON_VALUE(), JSON_QUERY(), JSON_EXISTS() + */ +typedef struct JsonExpr +{ + Expr xpr; + JsonExprOp op; /* json function ID */ + Node *formatted_expr; /* formatted context item expression */ + JsonCoercion *result_coercion; /* resulting coercion to RETURNING type */ + JsonFormat *format; /* context item format (JSON/JSONB) */ + Node *path_spec; /* JSON path specification expression */ + List *passing_names; /* PASSING argument names */ + List *passing_values; /* PASSING argument values */ + JsonReturning *returning; /* RETURNING clause type/format info */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + JsonItemCoercions *coercions; /* coercions for JSON_VALUE */ + JsonWrapper wrapper; /* WRAPPER for JSON_QUERY */ + bool omit_quotes; /* KEEP/OMIT QUOTES for JSON_QUERY */ + int location; /* token location, or -1 if unknown */ +} JsonExpr; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 5984dcfa4b..0954d9fc7b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -93,6 +93,7 @@ PG_KEYWORD("commit", COMMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("committed", COMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("compression", COMPRESSION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("concurrently", CONCURRENTLY, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("conditional", CONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("configuration", CONFIGURATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("conflict", CONFLICT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("connection", CONNECTION, UNRESERVED_KEYWORD, BARE_LABEL) @@ -147,11 +148,13 @@ PG_KEYWORD("double", DOUBLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("drop", DROP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("each", EACH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("empty", EMPTY_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("event", EVENT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("except", EXCEPT, RESERVED_KEYWORD, AS_LABEL) @@ -233,10 +236,14 @@ PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_exists", JSON_EXISTS, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) @@ -302,6 +309,7 @@ PG_KEYWORD("off", OFF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("offset", OFFSET, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("oids", OIDS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("old", OLD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("omit", OMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("on", ON, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("only", ONLY, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("operator", OPERATOR, UNRESERVED_KEYWORD, BARE_LABEL) @@ -344,6 +352,7 @@ PG_KEYWORD("procedures", PROCEDURES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("publication", PUBLICATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("quotes", QUOTES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("range", RANGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("read", READ, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("real", REAL, COL_NAME_KEYWORD, BARE_LABEL) @@ -414,6 +423,7 @@ PG_KEYWORD("stdout", STDOUT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("storage", STORAGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("stored", STORED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("string", STRING_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("subscription", SUBSCRIPTION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("substring", SUBSTRING, COL_NAME_KEYWORD, BARE_LABEL) @@ -449,6 +459,7 @@ PG_KEYWORD("types", TYPES_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uescape", UESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unbounded", UNBOUNDED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uncommitted", UNCOMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("unconditional", UNCONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unencrypted", UNENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("union", UNION, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("unique", UNIQUE, RESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/formatting.h b/src/include/utils/formatting.h index 0cad3a2709..d4ca0f42ff 100644 --- a/src/include/utils/formatting.h +++ b/src/include/utils/formatting.h @@ -17,7 +17,6 @@ #ifndef _FORMATTING_H_ #define _FORMATTING_H_ - extern char *str_tolower(const char *buff, size_t nbytes, Oid collid); extern char *str_toupper(const char *buff, size_t nbytes, Oid collid); extern char *str_initcap(const char *buff, size_t nbytes, Oid collid); @@ -29,5 +28,6 @@ extern char *asc_initcap(const char *buff, size_t nbytes); extern Datum parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, Oid *typid, int32 *typmod, int *tz, struct Node *escontext); +extern bool datetime_format_has_tz(const char *fmt_str); #endif diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index f5c7441f50..0299b182ca 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -421,6 +421,9 @@ extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, int estimated_len); +extern Jsonb *JsonbMakeEmptyArray(void); +extern Jsonb *JsonbMakeEmptyObject(void); +extern char *JsonbUnquote(Jsonb *jb); extern bool JsonbExtractScalar(JsonbContainer *jbc, JsonbValue *res); extern const char *JsonbTypeName(JsonbValue *val); diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index c677ac8ff7..ab543b9423 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -15,6 +15,7 @@ #define JSONFUNCS_H #include "common/jsonapi.h" +#include "nodes/nodes.h" #include "utils/jsonb.h" /* @@ -87,5 +88,9 @@ extern Datum datum_to_json(Datum val, JsonTypeCategory tcategory, extern Datum datum_to_jsonb(Datum val, JsonTypeCategory tcategory, Oid outfuncoid); extern Datum jsonb_from_text(text *js, bool unique_keys); +extern Datum json_populate_type(Datum json_val, Oid json_type, + Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext); #endif diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index f0181e045f..4dae78a98c 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -16,7 +16,9 @@ #include "fmgr.h" #include "nodes/pg_list.h" +#include "nodes/primnodes.h" #include "utils/jsonb.h" +#include "utils/jsonfuncs.h" typedef struct { @@ -184,6 +186,7 @@ extern bool jspGetBool(JsonPathItem *v); extern char *jspGetString(JsonPathItem *v, int32 *len); extern bool jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, int i); +extern bool jspIsMutable(JsonPath *path, List *varnames, List *varexprs); extern const char *jspOperationName(JsonPathItemType type); @@ -261,4 +264,28 @@ extern bool jspConvertRegexFlags(uint32 xflags, int *result, struct Node *escontext); +/* + * Evaluation of jsonpath + */ + +/* External variable passed into jsonpath. */ +typedef struct JsonPathVariable +{ + char *name; + Oid typid; + int32 typmod; + Datum value; + bool isnull; +} JsonPathVariable; + +/* SQL/JSON item */ +extern void JsonItemFromDatum(Datum val, Oid typid, int32 typmod, + JsonbValue *res); + +extern bool JsonPathExists(Datum jb, JsonPath *path, List *vars, bool *error); +extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, + bool *empty, bool *error, List *vars); +extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, + bool *error, List *vars); + #endif diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer index 435c139ec2..b2aa44f36d 100644 --- a/src/interfaces/ecpg/preproc/ecpg.trailer +++ b/src/interfaces/ecpg/preproc/ecpg.trailer @@ -651,6 +651,34 @@ var_type: simple_type $$.type_index = mm_strdup("-1"); $$.type_sizeof = NULL; } + | STRING_P + { + if (INFORMIX_MODE) + { + /* In Informix mode, "string" is automatically a typedef */ + $$.type_enum = ECPGt_string; + $$.type_str = mm_strdup("char"); + $$.type_dimension = mm_strdup("-1"); + $$.type_index = mm_strdup("-1"); + $$.type_sizeof = NULL; + } + else + { + /* Otherwise, legal only if user typedef'ed it */ + struct typedefs *this = get_typedef("string", false); + + $$.type_str = (this->type->type_enum == ECPGt_varchar || this->type->type_enum == ECPGt_bytea) ? EMPTY : mm_strdup(this->name); + $$.type_enum = this->type->type_enum; + $$.type_dimension = this->type->type_dimension; + $$.type_index = this->type->type_index; + if (this->type->type_sizeof && strlen(this->type->type_sizeof) != 0) + $$.type_sizeof = this->type->type_sizeof; + else + $$.type_sizeof = cat_str(3, mm_strdup("sizeof("), mm_strdup(this->name), mm_strdup(")")); + + struct_member_list[struct_level] = ECPGstruct_member_dup(this->struct_member_list); + } + } | INTERVAL ecpg_interval { $$.type_enum = ECPGt_interval; diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out new file mode 100644 index 0000000000..8b87580752 --- /dev/null +++ b/src/test/regress/expected/json_sqljson.out @@ -0,0 +1,18 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); +ERROR: JSON_EXISTS() is not yet implemented for the json type +LINE 1: SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_VALUE +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); +ERROR: JSON_VALUE() is not yet implemented for the json type +LINE 1: SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_QUERY +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); +ERROR: JSON_QUERY() is not yet implemented for the json type +LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out new file mode 100644 index 0000000000..22f8679917 --- /dev/null +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -0,0 +1,1042 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL::jsonb, '$'); + json_exists +------------- + +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb 'null', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_EXISTS(jsonb 'null', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{}', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + json_exists +------------- + f +(1 row) + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + json_exists +------------- + t +(1 row) + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); + json_exists +------------- + 1 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); + json_exists +------------- + 0 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); + json_exists +------------- + true +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +ERROR: cannot cast type boolean to jsonb +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +ERROR: cannot cast type boolean to real +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_EXISTS() +LINE 1: ...CT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSO... + ^ +-- JSON_VALUE +SELECT JSON_VALUE(NULL::jsonb, '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$'); + json_value +------------ + true +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + json_value +------------ + t +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); + json_value +------------ + 123 +(1 row) + +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); +ERROR: SQL/JSON item cannot be cast to target type +SELECT JSON_VALUE(jsonb '1.23', '$'); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "1.23" +SELECT JSON_VALUE(jsonb '"aaa"', '$'); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); + json_value +------------ + aa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "aaa" +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); + json_value +------------ + 111 +(1 row) + +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + ?column? +------------ + 03-01-2017 +(1 row) + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '[]', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '{}', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '1', '$.a'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); + json_value +------------ + error +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 3 +(1 row) + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); + json_value +------------ + 0 +(1 row) + +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: " " +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 5 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_VALUE() +LINE 1: ...CT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSO... + ^ +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + x | y +---+---- + 0 | -2 + 1 | 2 + 2 | -1 +(3 rows) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); + json_value +------------ + (1,2) +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + json_value +------------ + (1,2) +(1 row) + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); + json_value +-------------------------- + Tue Feb 20 18:34:56 2018 +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- JSON_QUERY +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + json_query | json_query | json_query | json_query | json_query +--------------------+--------------------+--------------------+----------------------+---------------------- + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, null, "2"] | [1, null, "2"] | [1, null, "2"] | [[1, null, "2"]] | [[1, null, "2"]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] +(6 rows) + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + unspec | without | with cond | with uncond | with +--------------------+--------------------+---------------------+----------------------+---------------------- + | | | | + | | | | + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, 2, 3] | [1, 2, 3] | [1, 2, 3] | [[1, 2, 3]] | [[1, 2, 3]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] + | | [1, "2", null, [3]] | [1, "2", null, [3]] | [1, "2", null, [3]] +(9 rows) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + json_query +------------ + \x616161 +(1 row) + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTE... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTE... + ^ +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_QUERY should return singleton item without wrapper +HINT: Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array. +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); + json_query +------------ + [1, +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); +ERROR: expected JSON array +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + x | y | list +---+---+-------------- + 0 | 0 | [] + 0 | 1 | [1] + 0 | 2 | [1, 2] + 0 | 3 | [1, 2, 3] + 0 | 4 | [1, 2, 3, 4] + 1 | 0 | [] + 1 | 1 | [1] + 1 | 2 | [1, 2] + 1 | 3 | [1, 2, 3] + 1 | 4 | [1, 2, 3, 4] + 2 | 0 | [] + 2 | 1 | [] + 2 | 2 | [2] + 2 | 3 | [2, 3] + 2 | 4 | [2, 3, 4] + 3 | 0 | [] + 3 | 1 | [] + 3 | 2 | [] + 3 | 3 | [3] + 3 | 4 | [3, 4] + 4 | 0 | [] + 4 | 1 | [] + 4 | 2 | [] + 4 | 3 | [] + 4 | 4 | [4] +(25 rows) + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); + json_query +----------------------------------------------------- + (1,aaa,"[1, ""2"", {}]","{""x"": [1, ""2"", {}]}",) +(1 row) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); + unnest +------------------------ + {"a": 1, "b": ["foo"]} + {"a": 2, "c": {}} + 123 +(3 rows) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); + json_query +-------------- + {1,2,NULL,3} +(1 row) + +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); + json_query +------------ + 1 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- Test constraints +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); +\d test_jsonb_constraints + Table "public.test_jsonb_constraints" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+-------------------------------------------------------------------------------- + js | text | | | + i | integer | | | + x | jsonb | | | JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +Check constraints: + "test_jsonb_constraint1" CHECK (js IS JSON) + "test_jsonb_constraint2" CHECK (JSON_EXISTS(js::jsonb, '$."a"' PASSING i + 5 AS int, i::text AS txt, ARRAY[1, 2, 3] AS arr)) + "test_jsonb_constraint3" CHECK (JSON_VALUE(js::jsonb, '$."a"' RETURNING integer DEFAULT ('12'::text || i)::integer ON EMPTY ERROR ON ERROR) > i) + "test_jsonb_constraint4" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb) + "test_jsonb_constraint5" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C")) + "test_jsonb_constraint6" CHECK (JSON_EXISTS(js::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2) + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + check_clause +-------------------------------------------------------------------------------------------------------------------------- + ((JSON_EXISTS((js)::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2)) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C"))) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb)) + ((JSON_VALUE((js)::jsonb, '$."a"' RETURNING integer DEFAULT (('12'::text || i))::integer ON EMPTY ERROR ON ERROR) > i)) + ((js IS JSON)) + (JSON_EXISTS((js)::jsonb, '$."a"' PASSING (i + 5) AS int, (i)::text AS txt, ARRAY[1, 2, 3] AS arr)) +(6 rows) + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +WHERE constraint_name LIKE 'test_jsonb_constraint%'; +ERROR: syntax error at or near "WHERE" +LINE 1: WHERE constraint_name LIKE 'test_jsonb_constraint%'; + ^ +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint1" +DETAIL: Failing row contains (, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains (1, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('[]'); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ([], null, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ({"b": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint3" +DETAIL: Failing row contains ({"a": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint5" +DETAIL: Failing row contains ({"a": 7}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint4" +DETAIL: Failing row contains ({"a": 10}, 1, [1, 2]). +DROP TABLE test_jsonb_constraints; +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 4df9d8503b..9b0ecf049d 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -103,7 +103,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo # ---------- # Another group of parallel tests (JSON related) # ---------- -test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson +test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson json_sqljson jsonb_sqljson # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql new file mode 100644 index 0000000000..4f30fa46b9 --- /dev/null +++ b/src/test/regress/sql/json_sqljson.sql @@ -0,0 +1,11 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + +-- JSON_QUERY + +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql new file mode 100644 index 0000000000..b8a6148b7b --- /dev/null +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -0,0 +1,327 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL::jsonb, '$'); + +SELECT JSON_EXISTS(jsonb '[]', '$'); +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + +SELECT JSON_EXISTS(jsonb '1', '$'); +SELECT JSON_EXISTS(jsonb 'null', '$'); +SELECT JSON_EXISTS(jsonb '[]', '$'); + +SELECT JSON_EXISTS(jsonb '1', '$.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_EXISTS(jsonb 'null', '$.a'); +SELECT JSON_EXISTS(jsonb '[]', '$.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); +SELECT JSON_EXISTS(jsonb '{}', '$.a'); +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSON); -- RETURNING FORMAT not allowed + + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL::jsonb, '$'); + +SELECT JSON_VALUE(jsonb 'null', '$'); +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + +SELECT JSON_VALUE(jsonb 'true', '$'); +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + +SELECT JSON_VALUE(jsonb '123', '$'); +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1.23', '$'); +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '"aaa"', '$'); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); + +SELECT JSON_VALUE(jsonb '[]', '$'); +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '{}', '$'); +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1', '$.a'); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed + +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- JSON_QUERY + +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); + +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); + +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); + +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- Test constraints + +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); + +\d test_jsonb_constraints + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; +WHERE constraint_name LIKE 'test_jsonb_constraint%'; + +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +INSERT INTO test_jsonb_constraints VALUES ('[]'); +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); + +DROP TABLE test_jsonb_constraints; + +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e746a481d6..e9f34de8b8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1240,6 +1240,7 @@ Join JoinCostWorkspace JoinDomain JoinExpr +JsonFuncExpr JoinHashEntry JoinPath JoinPathExtraData @@ -1250,18 +1251,31 @@ JsObject JsValue JsonAggConstructor JsonAggState +JsonArgument JsonArrayAgg JsonArrayConstructor JsonArrayQueryConstructor JsonBaseObjectInfo +JsonBehavior +JsonBehaviorType +JsonCoercion +JsonCoercionState +JsonCommon JsonConstructorExpr JsonConstructorExprState JsonConstructorType JsonEncoding +JsonExpr +JsonExprOp +JsonExprPostEvalState +JsonExprPreEvalState +JsonExprState JsonFormat JsonFormatType JsonHashEntry JsonIsPredicate +JsonItemCoercions +JsonItemCoercionsState JsonIterateStringValuesAction JsonKeyValue JsonLexContext @@ -1279,6 +1293,7 @@ JsonParseContext JsonParseErrorType JsonPath JsonPathBool +JsonPathDatatypeStatus JsonPathExecContext JsonPathExecResult JsonPathGinAddPathItemFunc @@ -1291,10 +1306,14 @@ JsonPathGinPathItem JsonPathItem JsonPathItemType JsonPathKeyword +JsonPathMutableContext JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback JsonPathString +JsonPathVarCallback +JsonPathVariable +JsonPathVariableEvalContext JsonQuotes JsonReturning JsonScalarExpr @@ -1312,6 +1331,7 @@ JsonValueExpr JsonValueList JsonValueListIterator JsonValueType +JsonWrapper Jsonb JsonbAggState JsonbContainer -- 2.35.3
v10-0001-Some-refactoring-to-export-json-b-conversion-fun.patch (application/octet-stream, 11.2K) download From 64f14a7c559a82a159fdebf8471bf901c59721d9 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Fri, 21 Jul 2023 11:46:56 +0900 Subject: [PATCH v10 1/6] Some refactoring to export json(b) conversion functions This is to export datum_to_json(), datum_to_jsonb(), and jsonb_from_cstring(), though the last one is exported as jsonb_from_text(). A subsequent commit to add new SQL/JSON constructor functions will need them for calling from the executor. Discussion: https://postgr.es/m/20230720160252.ldk7jy6jqclxfxkq%40alvherre.pgsql --- src/backend/utils/adt/json.c | 59 ++++++++++++++++++------------ src/backend/utils/adt/jsonb.c | 67 +++++++++++++++++++++++++---------- src/include/utils/jsonfuncs.h | 5 +++ 3 files changed, 90 insertions(+), 41 deletions(-) diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index f6bef9c148..e405791f5d 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -91,9 +91,9 @@ static void array_dim_to_json(StringInfo result, int dim, int ndims, int *dims, bool use_line_feeds); static void array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds); -static void datum_to_json(Datum val, bool is_null, StringInfo result, - JsonTypeCategory tcategory, Oid outfuncoid, - bool key_scalar); +static void datum_to_json_internal(Datum val, bool is_null, StringInfo result, + JsonTypeCategory tcategory, Oid outfuncoid, + bool key_scalar); static void add_json(Datum val, bool is_null, StringInfo result, Oid val_type, bool key_scalar); static text *catenate_stringinfo_string(StringInfo buffer, const char *addon); @@ -173,9 +173,9 @@ json_recv(PG_FUNCTION_ARGS) * it's of an acceptable type, and force it to be quoted. */ static void -datum_to_json(Datum val, bool is_null, StringInfo result, - JsonTypeCategory tcategory, Oid outfuncoid, - bool key_scalar) +datum_to_json_internal(Datum val, bool is_null, StringInfo result, + JsonTypeCategory tcategory, Oid outfuncoid, + bool key_scalar) { char *outputstr; text *jsontext; @@ -421,8 +421,9 @@ array_dim_to_json(StringInfo result, int dim, int ndims, int *dims, Datum *vals, if (dim + 1 == ndims) { - datum_to_json(vals[*valcount], nulls[*valcount], result, tcategory, - outfuncoid, false); + datum_to_json_internal(vals[*valcount], nulls[*valcount], + result, tcategory, + outfuncoid, false); (*valcount)++; } else @@ -549,7 +550,8 @@ composite_to_json(Datum composite, StringInfo result, bool use_line_feeds) json_categorize_type(att->atttypid, false, &tcategory, &outfuncoid); - datum_to_json(val, isnull, result, tcategory, outfuncoid, false); + datum_to_json_internal(val, isnull, result, tcategory, outfuncoid, + false); } appendStringInfoChar(result, '}'); @@ -584,7 +586,8 @@ add_json(Datum val, bool is_null, StringInfo result, json_categorize_type(val_type, false, &tcategory, &outfuncoid); - datum_to_json(val, is_null, result, tcategory, outfuncoid, key_scalar); + datum_to_json_internal(val, is_null, result, tcategory, outfuncoid, + key_scalar); } /* @@ -704,7 +707,6 @@ to_json(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - StringInfo result; JsonTypeCategory tcategory; Oid outfuncoid; @@ -716,11 +718,23 @@ to_json(PG_FUNCTION_ARGS) json_categorize_type(val_type, false, &tcategory, &outfuncoid); - result = makeStringInfo(); + PG_RETURN_DATUM(datum_to_json(val, tcategory, outfuncoid)); +} - datum_to_json(val, false, result, tcategory, outfuncoid, false); +/* + * Turn a Datum into JSON text. + * + * tcategory and outfuncoid are from a previous call to json_categorize_type. + */ +Datum +datum_to_json(Datum val, JsonTypeCategory tcategory, Oid outfuncoid) +{ + StringInfo result = makeStringInfo(); - PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); + datum_to_json_internal(val, false, result, tcategory, outfuncoid, + false); + + return PointerGetDatum(cstring_to_text_with_len(result->data, result->len)); } /* @@ -780,8 +794,8 @@ json_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null) /* fast path for NULLs */ if (PG_ARGISNULL(1)) { - datum_to_json((Datum) 0, true, state->str, JSONTYPE_NULL, - InvalidOid, false); + datum_to_json_internal((Datum) 0, true, state->str, JSONTYPE_NULL, + InvalidOid, false); PG_RETURN_POINTER(state); } @@ -795,8 +809,8 @@ json_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null) appendStringInfoString(state->str, "\n "); } - datum_to_json(val, false, state->str, state->val_category, - state->val_output_func, false); + datum_to_json_internal(val, false, state->str, state->val_category, + state->val_output_func, false); /* * The transition type for json_agg() is declared to be "internal", which @@ -1059,8 +1073,8 @@ json_object_agg_transfn_worker(FunctionCallInfo fcinfo, key_offset = out->len; - datum_to_json(arg, false, out, state->key_category, - state->key_output_func, true); + datum_to_json_internal(arg, false, out, state->key_category, + state->key_output_func, true); if (unique_keys) { @@ -1082,8 +1096,9 @@ json_object_agg_transfn_worker(FunctionCallInfo fcinfo, else arg = PG_GETARG_DATUM(2); - datum_to_json(arg, PG_ARGISNULL(2), state->str, state->val_category, - state->val_output_func, false); + datum_to_json_internal(arg, PG_ARGISNULL(2), state->str, + state->val_category, + state->val_output_func, false); PG_RETURN_POINTER(state); } diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index fc64f56868..5ea582a888 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -59,9 +59,9 @@ static void array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *di Datum *vals, bool *nulls, int *valcount, JsonTypeCategory tcategory, Oid outfuncoid); static void array_to_jsonb_internal(Datum array, JsonbInState *result); -static void datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, - JsonTypeCategory tcategory, Oid outfuncoid, - bool key_scalar); +static void datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result, + JsonTypeCategory tcategory, Oid outfuncoid, + bool key_scalar); static void add_jsonb(Datum val, bool is_null, JsonbInState *result, Oid val_type, bool key_scalar); static JsonbParseState *clone_parse_state(JsonbParseState *state); @@ -141,6 +141,19 @@ jsonb_send(PG_FUNCTION_ARGS) PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); } +/* + * jsonb_from_text + * + * Turns json text string into a jsonb Datum. + */ +Datum +jsonb_from_text(text *js) +{ + return jsonb_from_cstring(VARDATA_ANY(js), + VARSIZE_ANY_EXHDR(js), + NULL); +} + /* * Get the type name of a jsonb container. */ @@ -622,9 +635,9 @@ add_indent(StringInfo out, bool indent, int level) * will be thrown. */ static void -datum_to_jsonb(Datum val, bool is_null, JsonbInState *result, - JsonTypeCategory tcategory, Oid outfuncoid, - bool key_scalar) +datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result, + JsonTypeCategory tcategory, Oid outfuncoid, + bool key_scalar) { char *outputstr; bool numeric_error; @@ -859,8 +872,8 @@ array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *dims, Datum *v { if (dim + 1 == ndims) { - datum_to_jsonb(vals[*valcount], nulls[*valcount], result, tcategory, - outfuncoid, false); + datum_to_jsonb_internal(vals[*valcount], nulls[*valcount], result, tcategory, + outfuncoid, false); (*valcount)++; } else @@ -982,7 +995,8 @@ composite_to_jsonb(Datum composite, JsonbInState *result) json_categorize_type(att->atttypid, true, &tcategory, &outfuncoid); - datum_to_jsonb(val, isnull, result, tcategory, outfuncoid, false); + datum_to_jsonb_internal(val, isnull, result, tcategory, outfuncoid, + false); } result->res = pushJsonbValue(&result->parseState, WJB_END_OBJECT, NULL); @@ -1018,9 +1032,11 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, json_categorize_type(val_type, true, &tcategory, &outfuncoid); - datum_to_jsonb(val, is_null, result, tcategory, outfuncoid, key_scalar); + datum_to_jsonb_internal(val, is_null, result, tcategory, outfuncoid, + key_scalar); } + /* * Is the given type immutable when coming out of a JSONB context? * @@ -1072,7 +1088,6 @@ to_jsonb(PG_FUNCTION_ARGS) { Datum val = PG_GETARG_DATUM(0); Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); - JsonbInState result; JsonTypeCategory tcategory; Oid outfuncoid; @@ -1084,11 +1099,25 @@ to_jsonb(PG_FUNCTION_ARGS) json_categorize_type(val_type, true, &tcategory, &outfuncoid); + PG_RETURN_DATUM(datum_to_jsonb(val, tcategory, outfuncoid)); +} + +/* + * Turn a Datum into jsonb. + * + * tcategory and outfuncoid are from a previous call to json_categorize_type. + */ +Datum +datum_to_jsonb(Datum val, JsonTypeCategory tcategory, Oid outfuncoid) +{ + JsonbInState result; + memset(&result, 0, sizeof(JsonbInState)); - datum_to_jsonb(val, false, &result, tcategory, outfuncoid, false); + datum_to_jsonb_internal(val, false, &result, tcategory, outfuncoid, + false); - PG_RETURN_POINTER(JsonbValueToJsonb(result.res)); + return JsonbPGetDatum(JsonbValueToJsonb(result.res)); } Datum @@ -1525,8 +1554,8 @@ jsonb_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null) memset(&elem, 0, sizeof(JsonbInState)); - datum_to_jsonb(val, PG_ARGISNULL(1), &elem, state->val_category, - state->val_output_func, false); + datum_to_jsonb_internal(val, PG_ARGISNULL(1), &elem, state->val_category, + state->val_output_func, false); jbelem = JsonbValueToJsonb(elem.res); @@ -1726,8 +1755,8 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo, memset(&elem, 0, sizeof(JsonbInState)); - datum_to_jsonb(val, false, &elem, state->key_category, - state->key_output_func, true); + datum_to_jsonb_internal(val, false, &elem, state->key_category, + state->key_output_func, true); jbkey = JsonbValueToJsonb(elem.res); @@ -1735,8 +1764,8 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo, memset(&elem, 0, sizeof(JsonbInState)); - datum_to_jsonb(val, PG_ARGISNULL(2), &elem, state->val_category, - state->val_output_func, false); + datum_to_jsonb_internal(val, PG_ARGISNULL(2), &elem, state->val_category, + state->val_output_func, false); jbval = JsonbValueToJsonb(elem.res); diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index 121dfd5d24..2ad648d1b8 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -82,5 +82,10 @@ typedef enum extern void json_categorize_type(Oid typoid, bool is_jsonb, JsonTypeCategory *tcategory, Oid *outfuncoid); +extern Datum datum_to_json(Datum val, JsonTypeCategory tcategory, + Oid outfuncoid); +extern Datum datum_to_jsonb(Datum val, JsonTypeCategory tcategory, + Oid outfuncoid); +extern Datum jsonb_from_text(text *js); #endif -- 2.35.3
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-23 08:17  jian he <jian.universality@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  2 siblings, 1 reply; 300+ messages in thread

From: jian he @ 2023-07-23 08:17 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

hi
based on v10*.patch. questions/ideas about the doc.

> json_exists ( context_item, path_expression [ PASSING { value AS varname } [, ...]] [ RETURNING data_type ] [ { TRUE | FALSE | UNKNOWN | ERROR } ON ERROR ])
> Returns true if the SQL/JSON path_expression applied to the context_item using the values yields any items. The ON ERROR clause specifies what is returned if an error occurs. Note that if the path_expression is strict, an error is generated if it yields no items. The default value is UNKNOWN which causes a NULL result.

only SELECT JSON_EXISTS(NULL::jsonb, '$'); will cause a null result.
In lex mode, if yield no items return false, no error will return,
even error on error.
Only  case error will happen, strict mode error on error. (select
json_exists(jsonb '{"a": [1,2,3]}', 'strict $.b' error on error)

so I came up with the following:
Returns true if the SQL/JSON path_expression applied to the
context_item using the values yields any items. The ON ERROR clause
specifies what is returned if an error occurs, if not specified, the
default value is false when it yields no items.
Note that if the path_expression is strict, ERROR ON ERROR specified,
an error is generated if it yields no items.
--------------------------------------------------------------------------------------------------
/* --first branch of json_table_column spec.

name type [ PATH json_path_specification ]
        [ { WITHOUT | WITH { CONDITIONAL | [UNCONDITIONAL] } } [ ARRAY
] WRAPPER ]
        [ { KEEP | OMIT } QUOTES [ ON SCALAR STRING ] ]
        [ { ERROR | NULL | DEFAULT expression } ON EMPTY ]
        [ { ERROR | NULL | DEFAULT expression } ON ERROR ]
*/
I am not sure what " [ ON SCALAR STRING ]"  means. There is no test on this.
i wonder how to achieve the following  query with json_table:
select json_query(jsonb '"world"', '$' returning text keep quotes) ;

the following case will fail.
SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text PATH
'$' keep quotes ON SCALAR STRING ));
ERROR:  cannot use OMIT QUOTES clause with scalar columns
LINE 1: ...T * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text ...
                                                             ^
error should be ERROR:  cannot use KEEP QUOTES clause with scalar columns?
LINE1 should be: SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS
(item text ...
--------------------------------------------------------------------------------
quote from json_query:
> This function must return a JSON string, so if the path expression returns multiple SQL/JSON items, you must wrap the result using the
> WITH WRAPPER clause.

I think the final result will be: if the RETURNING clause is not
specified, then the returned data type is jsonb. if multiple SQL/JSON
items returned, if not specified WITH WRAPPER, null will be returned.
------------------------------------------------------------------------------------
quote from json_query:
>  The ON ERROR and ON EMPTY clauses have similar semantics to those clauses for json_value.
quote from json_table:
> These clauses have the same syntax and semantics as for json_value and json_query.

it would be better in json_value syntax explicit mention: if not
explicitly mentioned, what will happen when on error, on empty
happened ?
-------------------------------------------------------------------------------------
> You can have only one ordinality column per table
but the regress test shows that you can have more than one ordinality column.
----------------------------------------------------------------------------
similar to here
https://git.postgresql.org/cgit/postgresql.git/tree/src/test/regress/expected/sqljson.out#n804
Maybe in file src/test/regress/sql/jsonb_sqljson.sql line 349, you can
also create a  table first. insert corner case data.
then split the very wide select query (more than 26 columns) into 4
small queries, better to view the expected result on the web.





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-26 08:10  Amit Langote <amitlangote09@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  2 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-07-26 08:10 UTC (permalink / raw)
  To: Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers; jian he <jian.universality@gmail.com>

On Fri, Jul 21, 2023 at 7:33 PM Amit Langote <amitlangote09@gmail.com> wrote:
> On Fri, Jul 21, 2023 at 1:02 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
> > On 2023-Jul-21, Amit Langote wrote:
> >
> > > I’m thinking of pushing 0001 and 0002 tomorrow barring objections.
> >
> > 0001 looks reasonable to me.  I think you asked whether to squash that
> > one with the other bugfix commit for the same code that you already
> > pushed to master; I think there's no point in committing as separate
> > patches, because the first one won't show up in the git_changelog output
> > as a single entity with the one in 16, so it'll just be additional
> > noise.
>
> OK, pushed 0001 to HEAD and b6e1157e7d + 0001 to 16.
>
> > I've looked at 0002 at various points in time and I think it looks
> > generally reasonable.  I think your removal of a couple of newlines
> > (where originally two appear in sequence) is unwarranted; that the name
> > to_json[b]_worker is ugly for exported functions (maybe "datum_to_json"
> > would be better, or you may have better ideas);
>
> Went with datum_to_json[b].  Created a separate refactoring patch for
> this, attached as 0001.
>
> Created another refactoring patch for the hunks related to renaming of
> a nonterminal in gram.y, attached as 0002.
>
> > and that the omission of
> > the stock comment in the new stanzas in FigureColnameInternal() is
> > strange.
>
> Yes, fixed.
>
> >  But I don't have anything serious.  Do add some ecpg tests ...
>
> Added.
>
> > Also, remember to pgindent and bump catversion, if you haven't already.
>
> Will do.  Wasn't sure myself whether the catversion should be bumped,
> but I suppose it must be because ruleutils.c has changed.
>
> Attaching latest patches.  Will push 0001, 0002, and 0003 on Monday to
> avoid worrying about the buildfarm on a Friday evening.

And pushed.

Will post the remaining patches after addressing jian he's comments.

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* RE: remaining sql/json patches
@ 2023-07-27 09:36  Shinoda, Noriyoshi (HPE Services Japan - FSIP) <noriyoshi.shinoda@hpe.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Shinoda, Noriyoshi (HPE Services Japan - FSIP) @ 2023-07-27 09:36 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers; jian he <jian.universality@gmail.com>

Hi,
Thank you for developing such a great feature. The attached patch formats the documentation like any other function definition:
- Added right parenthesis to json function calls.
- Added <returnvalue> to json functions.
- Added a space to the 'expression' part of the json_scalar function.
- Added a space to the 'expression' part of the json_serialize function.

It seems that the three functions added this time do not have tuples in the pg_proc catalog. Is it unnecessary?

Regards,
Noriyoshi Shinoda
-----Original Message-----
From: Amit Langote <amitlangote09@gmail.com> 
Sent: Wednesday, July 26, 2023 5:10 PM
To: Alvaro Herrera <alvherre@alvh.no-ip.org>
Cc: Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; PostgreSQL-development <pgsql-hackers@postgresql.org>; jian he <jian.universality@gmail.com>
Subject: Re: remaining sql/json patches

On Fri, Jul 21, 2023 at 7:33 PM Amit Langote <amitlangote09@gmail.com> wrote:
> On Fri, Jul 21, 2023 at 1:02 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
> > On 2023-Jul-21, Amit Langote wrote:
> >
> > > I’m thinking of pushing 0001 and 0002 tomorrow barring objections.
> >
> > 0001 looks reasonable to me.  I think you asked whether to squash 
> > that one with the other bugfix commit for the same code that you 
> > already pushed to master; I think there's no point in committing as 
> > separate patches, because the first one won't show up in the 
> > git_changelog output as a single entity with the one in 16, so it'll 
> > just be additional noise.
>
> OK, pushed 0001 to HEAD and b6e1157e7d + 0001 to 16.
>
> > I've looked at 0002 at various points in time and I think it looks 
> > generally reasonable.  I think your removal of a couple of newlines 
> > (where originally two appear in sequence) is unwarranted; that the 
> > name to_json[b]_worker is ugly for exported functions (maybe "datum_to_json"
> > would be better, or you may have better ideas);
>
> Went with datum_to_json[b].  Created a separate refactoring patch for 
> this, attached as 0001.
>
> Created another refactoring patch for the hunks related to renaming of 
> a nonterminal in gram.y, attached as 0002.
>
> > and that the omission of
> > the stock comment in the new stanzas in FigureColnameInternal() is 
> > strange.
>
> Yes, fixed.
>
> >  But I don't have anything serious.  Do add some ecpg tests ...
>
> Added.
>
> > Also, remember to pgindent and bump catversion, if you haven't already.
>
> Will do.  Wasn't sure myself whether the catversion should be bumped, 
> but I suppose it must be because ruleutils.c has changed.
>
> Attaching latest patches.  Will push 0001, 0002, and 0003 on Monday to 
> avoid worrying about the buildfarm on a Friday evening.

And pushed.

Will post the remaining patches after addressing jian he's comments.

--
Thanks, Amit Langote
EDB: http://www.enterprisedb.com 




Attachments:

json_func_doc_v1.diff (application/octet-stream, 2.1K) download diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index dcc9d6f59d..be2f54c914 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16008,7 +16008,8 @@ table2-mapping <function>json</function> ( <replaceable>expression</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional></optional> - <optional> { <literal>WITH</literal> | <literal>WITHOUT</literal> } <literal>UNIQUE</literal> <optional> <literal>KEYS</literal> </optional></optional> + <optional> { <literal>WITH</literal> | <literal>WITHOUT</literal> } <literal>UNIQUE</literal> <optional> <literal>KEYS</literal> </optional></optional> ) + <returnvalue>json</returnvalue> </para> <para> Converts a given expression specified as <type>text</type> or @@ -16029,7 +16030,7 @@ table2-mapping <entry role="func_table_entry"> <para role="func_signature"> <indexterm><primary>json_scalar</primary></indexterm> - <function>json_scalar</function> (<replaceable>expression</replaceable>) + <function>json_scalar</function> ( <replaceable>expression</replaceable> ) </para> <para> Converts a given SQL scalar value into a JSON scalar value. @@ -16052,7 +16053,7 @@ table2-mapping <para role="func_signature"> <function>json_serialize</function> ( <replaceable>expression</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> - <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional>) + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional> ) </para> <para> Converts an SQL/JSON expression into a character or binary string. The
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-07-28 06:57  Amit Langote <amitlangote09@gmail.com>
  parent: Shinoda, Noriyoshi (HPE Services Japan - FSIP) <noriyoshi.shinoda@hpe.com>
  0 siblings, 0 replies; 300+ messages in thread

From: Amit Langote @ 2023-07-28 06:57 UTC (permalink / raw)
  To: Shinoda, Noriyoshi (HPE Services Japan - FSIP) <noriyoshi.shinoda@hpe.com>; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers; jian he <jian.universality@gmail.com>

Hello,

On Thu, Jul 27, 2023 at 6:36 PM Shinoda, Noriyoshi (HPE Services Japan
- FSIP) <noriyoshi.shinoda@hpe.com> wrote:
> Hi,
> Thank you for developing such a great feature. The attached patch formats the documentation like any other function definition:
> - Added right parenthesis to json function calls.
> - Added <returnvalue> to json functions.
> - Added a space to the 'expression' part of the json_scalar function.
> - Added a space to the 'expression' part of the json_serialize function.

Thanks for checking and the patch.  Will push shortly.

> It seems that the three functions added this time do not have tuples in the pg_proc catalog. Is it unnecessary?

Yes.  These are not functions that get pg_proc entries, but SQL
constructs that *look like* functions, similar to XMLEXISTS(), etc.

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-08-04 10:02  Erik Rijkers <er@xs4all.nl>
  parent: Amit Langote <amitlangote09@gmail.com>
  2 siblings, 1 reply; 300+ messages in thread

From: Erik Rijkers @ 2023-08-04 10:02 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: Andrew Dunstan <andrew@dunslane.net>; pgsql-hackers; jian he <jian.universality@gmail.com>

Op 7/21/23 om 12:33 schreef Amit Langote:
> 
> Thanks for taking a look.
> 

Hi Amit,

Is there any chance to rebase the outstanding SQL/JSON patches, (esp. 
json_query)?

Thanks!

Erik Rijkers






^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-08-04 10:05  Amit Langote <amitlangote09@gmail.com>
  parent: Erik Rijkers <er@xs4all.nl>
  0 siblings, 0 replies; 300+ messages in thread

From: Amit Langote @ 2023-08-04 10:05 UTC (permalink / raw)
  To: Erik Rijkers <er@xs4all.nl>; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; pgsql-hackers; jian he <jian.universality@gmail.com>

Hi,

On Fri, Aug 4, 2023 at 19:01 Erik Rijkers <er@xs4all.nl> wrote:

> Op 7/21/23 om 12:33 schreef Amit Langote:
> >
> > Thanks for taking a look.
> >
>
> Hi Amit,
>
> Is there any chance to rebase the outstanding SQL/JSON patches, (esp.
> json_query)?


Yes, working on it.  Will post a WIP shortly.

> --
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-08-04 12:21  Amit Langote <amitlangote09@gmail.com>
  parent: jian he <jian.universality@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-08-04 12:21 UTC (permalink / raw)
  To: jian he <jian.universality@gmail.com>; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

Hi,

On Sun, Jul 23, 2023 at 5:17 PM jian he <jian.universality@gmail.com> wrote:
> hi
> based on v10*.patch. questions/ideas about the doc.

Thanks for taking a look.

> > json_exists ( context_item, path_expression [ PASSING { value AS varname } [, ...]] [ RETURNING data_type ] [ { TRUE | FALSE | UNKNOWN | ERROR } ON ERROR ])
> > Returns true if the SQL/JSON path_expression applied to the context_item using the values yields any items. The ON ERROR clause specifies what is returned if an error occurs. Note that if the path_expression is strict, an error is generated if it yields no items. The default value is UNKNOWN which causes a NULL result.
>
> only SELECT JSON_EXISTS(NULL::jsonb, '$'); will cause a null result.
> In lex mode, if yield no items return false, no error will return,
> even error on error.
> Only  case error will happen, strict mode error on error. (select
> json_exists(jsonb '{"a": [1,2,3]}', 'strict $.b' error on error)
>
> so I came up with the following:
> Returns true if the SQL/JSON path_expression applied to the
> context_item using the values yields any items. The ON ERROR clause
> specifies what is returned if an error occurs, if not specified, the
> default value is false when it yields no items.
> Note that if the path_expression is strict, ERROR ON ERROR specified,
> an error is generated if it yields no items.

OK, will change the text to say that the default ON ERROR behavior is
to return false.

> --------------------------------------------------------------------------------------------------
> /* --first branch of json_table_column spec.
>
> name type [ PATH json_path_specification ]
>         [ { WITHOUT | WITH { CONDITIONAL | [UNCONDITIONAL] } } [ ARRAY
> ] WRAPPER ]
>         [ { KEEP | OMIT } QUOTES [ ON SCALAR STRING ] ]
>         [ { ERROR | NULL | DEFAULT expression } ON EMPTY ]
>         [ { ERROR | NULL | DEFAULT expression } ON ERROR ]
> */
> I am not sure what " [ ON SCALAR STRING ]"  means. There is no test on this.

ON SCALAR STRING is just syntactic sugar.  KEEP/OMIT QUOTES specifies
the behavior when the result of JSON_QUERY() is a JSON scalar value.

> i wonder how to achieve the following  query with json_table:
> select json_query(jsonb '"world"', '$' returning text keep quotes) ;
>
> the following case will fail.
> SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text PATH
> '$' keep quotes ON SCALAR STRING ));
> ERROR:  cannot use OMIT QUOTES clause with scalar columns
> LINE 1: ...T * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text ...
>                                                              ^
> error should be ERROR:  cannot use KEEP QUOTES clause with scalar columns?
> LINE1 should be: SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS
> (item text ...

Hmm, yes, I think the code that produces the error is not trying hard
enough to figure out the actually specified QUOTES clause.  Fixed and
added new tests.

> --------------------------------------------------------------------------------
> quote from json_query:
> > This function must return a JSON string, so if the path expression returns multiple SQL/JSON items, you must wrap the result using the
> > WITH WRAPPER clause.
>
> I think the final result will be: if the RETURNING clause is not
> specified, then the returned data type is jsonb. if multiple SQL/JSON
> items returned, if not specified WITH WRAPPER, null will be returned.

I suppose you mean the following case:

SELECT JSON_QUERY(jsonb '[1,2]', '$[*]');
 json_query
------------

(1 row)

which with ERROR ON ERROR gives:

SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR);
ERROR:  JSON path expression in JSON_QUERY should return singleton
item without wrapper
HINT:  Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array.

The default return value for JSON_QUERY when an error occurs during
path expression evaluation is NULL.  I don't think that it needs to be
mentioned separately.

> ------------------------------------------------------------------------------------
> quote from json_query:
> >  The ON ERROR and ON EMPTY clauses have similar semantics to those clauses for json_value.
> quote from json_table:
> > These clauses have the same syntax and semantics as for json_value and json_query.
>
> it would be better in json_value syntax explicit mention: if not
> explicitly mentioned, what will happen when on error, on empty
> happened ?

OK, I've improved the text here.

> -------------------------------------------------------------------------------------
> > You can have only one ordinality column per table
> but the regress test shows that you can have more than one ordinality column.

Hmm, I am not sure why the code's allowing that.  Anyway, for the lack
any historical notes on why it should be allowed, I've fixed the code
to allow only one ordinality columns and modified the tests.

> ----------------------------------------------------------------------------
> similar to here
> https://git.postgresql.org/cgit/postgresql.git/tree/src/test/regress/expected/sqljson.out#n804
> Maybe in file src/test/regress/sql/jsonb_sqljson.sql line 349, you can
> also create a  table first. insert corner case data.
> then split the very wide select query (more than 26 columns) into 4
> small queries, better to view the expected result on the web.

OK, done.

I'm still finding things to fix here and there, but here's what I have
got so far.




--
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

Attachments: download all (3 files, .tar.gz)

v11-0003-Claim-SQL-standard-compliance-for-SQL-JSON-featu.patch (application/octet-stream, 2.3K) download From 639203dfb89a326dadc8b916405bb2d198ecba3e Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Tue, 18 Jul 2023 17:58:44 +0900 Subject: [PATCH v11 3/3] Claim SQL standard compliance for SQL/JSON features Discussion: https://postgr.es/m/d03d809c-d0fb-fd6a-1476-d6dc18ec940e@dunslane.net --- src/backend/catalog/sql_features.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt index b33065d7bf..b379c71df9 100644 --- a/src/backend/catalog/sql_features.txt +++ b/src/backend/catalog/sql_features.txt @@ -548,15 +548,15 @@ T811 Basic SQL/JSON constructor functions YES T812 SQL/JSON: JSON_OBJECTAGG YES T813 SQL/JSON: JSON_ARRAYAGG with ORDER BY YES T814 Colon in JSON_OBJECT or JSON_OBJECTAGG YES -T821 Basic SQL/JSON query operators NO +T821 Basic SQL/JSON query operators YES T822 SQL/JSON: IS JSON WITH UNIQUE KEYS predicate YES -T823 SQL/JSON: PASSING clause NO -T824 JSON_TABLE: specific PLAN clause NO -T825 SQL/JSON: ON EMPTY and ON ERROR clauses NO -T826 General value expression in ON ERROR or ON EMPTY clauses NO -T827 JSON_TABLE: sibling NESTED COLUMNS clauses NO -T828 JSON_QUERY NO -T829 JSON_QUERY: array wrapper options NO +T823 SQL/JSON: PASSING clause YES +T824 JSON_TABLE: specific PLAN clause YES +T825 SQL/JSON: ON EMPTY and ON ERROR clauses YES +T826 General value expression in ON ERROR or ON EMPTY clauses YES +T827 JSON_TABLE: sibling NESTED COLUMNS clauses YES +T828 JSON_QUERY YES +T829 JSON_QUERY: array wrapper options YES T830 Enforcing unique keys in SQL/JSON constructor functions YES T831 SQL/JSON path language: strict mode YES T832 SQL/JSON path language: item method YES @@ -565,7 +565,7 @@ T834 SQL/JSON path language: wildcard member accessor YES T835 SQL/JSON path language: filter expressions YES T836 SQL/JSON path language: starts with predicate YES T837 SQL/JSON path language: regex_like predicate YES -T838 JSON_TABLE: PLAN DEFAULT clause NO +T838 JSON_TABLE: PLAN DEFAULT clause YES T839 Formatted cast of datetimes to/from character strings NO T840 Hex integer literals in SQL/JSON path language YES T851 SQL/JSON: optional keywords for default syntax YES -- 2.35.3
v11-0002-JSON_TABLE.patch (application/octet-stream, 171.6K) download From d33679bd11136b5671edb932e0588a9643deb0d1 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Tue, 18 Jul 2023 17:58:39 +0900 Subject: [PATCH v11 2/3] JSON_TABLE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This feature allows jsonb data to be treated as a table and thus used in a FROM clause like other tabular data. Data can be selected from the jsonb using jsonpath expressions, and hoisted out of nested structures in the jsonb to form multiple rows, more or less like an outer join. This also adds the PLAN clauses for JSON_TABLE, which allow the user to specify how data from nested paths are joined, allowing considerable freedom in shaping the tabular output of JSON_TABLE. PLAN DEFAULT allows the user to specify the global strategies when dealing with sibling or child nested paths. The is often sufficient to achieve the necessary goal, and is considerably simpler than the full PLAN clause, which allows the user to specify the strategy to be used for each named nested path. Author: Nikita Glukhov <n.gluhov@postgrespro.ru> Author: Teodor Sigaev <teodor@sigaev.ru> Author: Oleg Bartunov <obartunov@gmail.com> Author: Alexander Korotkov <aekorotkov@gmail.com> Author: Andrew Dunstan <andrew@dunslane.net> Author: Amit Langote <amitlangote09@gmail.com> Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- doc/src/sgml/func.sgml | 496 ++++++++ src/backend/commands/explain.c | 8 +- src/backend/executor/execExprInterp.c | 5 + src/backend/executor/nodeTableFuncscan.c | 27 +- src/backend/nodes/makefuncs.c | 19 + src/backend/nodes/nodeFuncs.c | 30 + src/backend/parser/Makefile | 1 + src/backend/parser/gram.y | 476 +++++++- src/backend/parser/meson.build | 1 + src/backend/parser/parse_clause.c | 14 +- src/backend/parser/parse_expr.c | 18 +- src/backend/parser/parse_jsontable.c | 751 ++++++++++++ src/backend/parser/parse_relation.c | 5 +- src/backend/parser/parse_target.c | 3 + src/backend/utils/adt/jsonpath_exec.c | 543 +++++++++ src/backend/utils/adt/ruleutils.c | 279 ++++- src/include/nodes/execnodes.h | 2 + src/include/nodes/makefuncs.h | 2 + src/include/nodes/parsenodes.h | 90 ++ src/include/nodes/primnodes.h | 61 +- src/include/parser/kwlist.h | 4 + src/include/parser/parse_clause.h | 3 + src/include/utils/jsonpath.h | 3 + src/test/regress/expected/json_sqljson.out | 6 + src/test/regress/expected/jsonb_sqljson.out | 1186 +++++++++++++++++++ src/test/regress/sql/json_sqljson.sql | 4 + src/test/regress/sql/jsonb_sqljson.sql | 674 +++++++++++ src/tools/pgindent/typedefs.list | 13 + 28 files changed, 4695 insertions(+), 29 deletions(-) create mode 100644 src/backend/parser/parse_jsontable.c diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index b26fdd84b0..74dc55d978 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -17141,6 +17141,502 @@ array w/o UK? | t </sect2> + <sect2 id="functions-sqljson-table"> + <title>JSON_TABLE</title> + <indexterm> + <primary>json_table</primary> + </indexterm> + + <para> + <function>json_table</function> is an SQL/JSON function which + queries <acronym>JSON</acronym> data + and presents the results as a relational view, which can be accessed as a + regular SQL table. You can only use <function>json_table</function> inside the + <literal>FROM</literal> clause of a <literal>SELECT</literal> statement. + </para> + + <para> + Taking JSON data as input, <function>json_table</function> uses + a path expression to extract a part of the provided data that + will be used as a <firstterm>row pattern</firstterm> for the + constructed view. Each SQL/JSON item at the top level of the row pattern serves + as the source for a separate row in the constructed relational view. + </para> + + <para> + To split the row pattern into columns, <function>json_table</function> + provides the <literal>COLUMNS</literal> clause that defines the + schema of the created view. For each column to be constructed, + this clause provides a separate path expression that evaluates + the row pattern, extracts a JSON item, and returns it as a + separate SQL value for the specified column. If the required value + is stored in a nested level of the row pattern, it can be extracted + using the <literal>NESTED PATH</literal> subclause. Joining the + columns returned by <literal>NESTED PATH</literal> can add multiple + new rows to the constructed view. Such rows are called + <firstterm>child rows</firstterm>, as opposed to the <firstterm>parent row</firstterm> + that generates them. + </para> + + <para> + The rows produced by <function>JSON_TABLE</function> are laterally + joined to the row that generated them, so you do not have to explicitly join + the constructed view with the original table holding <acronym>JSON</acronym> + data. Optionally, you can specify how to join the columns returned + by <literal>NESTED PATH</literal> using the <literal>PLAN</literal> clause. + </para> + + <para> + Each <literal>NESTED PATH</literal> clause can generate one or more + columns. Columns produced by <literal>NESTED PATH</literal>s at the + same level are considered to be <firstterm>siblings</firstterm>, + while a column produced by a <literal>NESTED PATH</literal> is + considered to be a child of the column produced by a + <literal>NESTED PATH</literal> or row expression at a higher level. + Sibling columns are always joined first. Once they are processed, + the resulting rows are joined to the parent row. + </para> + + <para> + The syntax is: + </para> + +<synopsis> +JSON_TABLE ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> AS <replaceable>json_path_name</replaceable> </optional> <optional> PASSING { <replaceable>value</replaceable> AS <replaceable>varname</replaceable> } <optional>, ...</optional> </optional> + COLUMNS ( <replaceable class="parameter">json_table_column</replaceable> <optional>, ...</optional> ) + <optional> { <literal>ERROR</literal> | <literal>EMPTY</literal> } <literal>ON ERROR</literal> </optional> + <optional> + PLAN ( <replaceable class="parameter">json_table_plan</replaceable> ) | + PLAN DEFAULT ( { INNER | OUTER } <optional> , { CROSS | UNION } </optional> + | { CROSS | UNION } <optional> , { INNER | OUTER } </optional> ) + </optional> +) + +<phrase> +where <replaceable class="parameter">json_table_column</replaceable> is: +</phrase> + <replaceable>name</replaceable> <replaceable>type</replaceable> <optional> PATH <replaceable>json_path_specification</replaceable> </optional> + <optional> { WITHOUT | WITH { CONDITIONAL | <optional>UNCONDITIONAL</optional> } } <optional> ARRAY </optional> WRAPPER </optional> + <optional> { KEEP | OMIT } QUOTES <optional> ON SCALAR STRING </optional> </optional> + <optional> { ERROR | NULL | DEFAULT <replaceable>expression</replaceable> } ON EMPTY </optional> + <optional> { ERROR | NULL | DEFAULT <replaceable>expression</replaceable> } ON ERROR </optional> + | <replaceable>name</replaceable> <replaceable>type</replaceable> FORMAT <replaceable>json_representation</replaceable> + <optional> PATH <replaceable>json_path_specification</replaceable> </optional> + <optional> { WITHOUT | WITH { CONDITIONAL | <optional>UNCONDITIONAL</optional> } } <optional> ARRAY </optional> WRAPPER </optional> + <optional> { KEEP | OMIT } QUOTES <optional> ON SCALAR STRING </optional> </optional> + <optional> { ERROR | NULL | EMPTY { ARRAY | OBJECT } | DEFAULT <replaceable>expression</replaceable> } ON EMPTY </optional> + <optional> { ERROR | NULL | EMPTY { ARRAY | OBJECT } | DEFAULT <replaceable>expression</replaceable> } ON ERROR </optional> + | <replaceable>name</replaceable> <replaceable>type</replaceable> EXISTS <optional> PATH <replaceable>json_path_specification</replaceable> </optional> + <optional> { ERROR | TRUE | FALSE | UNKNOWN } ON ERROR </optional> + | NESTED PATH <replaceable>json_path_specification</replaceable> <optional> AS <replaceable>path_name</replaceable> </optional> + COLUMNS ( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + | <replaceable>name</replaceable> FOR ORDINALITY +<phrase> +<replaceable>json_table_plan</replaceable> is: +</phrase> + <replaceable>json_path_name</replaceable> <optional> { OUTER | INNER } <replaceable>json_table_plan_primary</replaceable> </optional> + | <replaceable>json_table_plan_primary</replaceable> { UNION <replaceable>json_table_plan_primary</replaceable> } <optional>...</optional> + | <replaceable>json_table_plan_primary</replaceable> { CROSS <replaceable>json_table_plan_primary</replaceable> } <optional>...</optional> +<phrase> +<replaceable>json_table_plan_primary</replaceable> is: +</phrase> + <replaceable>json_path_name</replaceable> | ( <replaceable>json_table_plan</replaceable> ) +</synopsis> + + <para> + Each syntax element is described below in more detail. + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional></literal> + </term> + <listitem> + <para> + The input data to query, the JSON path expression defining the query, + and an optional <literal>PASSING</literal> clause, which can provide data + values to the <replaceable>path_expression</replaceable>. + The result of the input data + evaluation is called the <firstterm>row pattern</firstterm>. The row + pattern is used as the source for row values in the constructed view. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>COLUMNS</literal>( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + The <literal>COLUMNS</literal> clause defining the schema of the + constructed view. In this clause, you must specify all the columns + to be filled with SQL/JSON items. + The <replaceable>json_table_column</replaceable> + expression has the following syntax variants: + </para> + + <variablelist> + <varlistentry> + <term> + <literal><replaceable>name</replaceable> <replaceable>type</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional></literal> + </term> + <listitem> + + <para> + Inserts a single SQL/JSON item into each row of + the specified column. + </para> + <para> + The provided <literal>PATH</literal> expression is evaluated and + the column is filled with the produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>ON EMPTY</literal> and + <literal>ON ERROR</literal> clauses to define how to handle missing values + or structural errors. + <literal>WRAPPER</literal> and <literal>QUOTES</literal> clauses can only + be used with JSON, array, and composite types. + These clauses have the same syntax and semantics as for + <function>json_value</function> and <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> <literal>FORMAT</literal> <replaceable>json_representation</replaceable> + <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a composite SQL/JSON + item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression is evaluated and + the column is filled with the produced SQL/JSON items, one for each row. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + In this case, the column name must correspond to one of the + keys within the SQL/JSON item produced by the row pattern. + </para> + <para> + Optionally, you can add <literal>WRAPPER</literal>, <literal>QUOTES</literal>, + <literal>ON EMPTY</literal> and <literal>ON ERROR</literal> clauses + to define additional settings for the returned SQL/JSON items. + These clauses have the same syntax and semantics as + for <function>json_query</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <replaceable>type</replaceable> + <literal>EXISTS</literal> <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional> + </term> + <listitem> + + <para> + Generates a column and inserts a boolean item into each row of this column. + </para> + <para> + The provided <literal>PATH</literal> expression is evaluated, + a check whether any SQL/JSON items were returned is done, and + the column is filled with the resulting boolean value, one for each row. + The specified <parameter>type</parameter> should have a cast from the + <type>boolean</type>. + If the <literal>PATH</literal> expression is omitted, + <function>JSON_TABLE</function> uses the + <literal>$.<replaceable>name</replaceable></literal> path expression, + where <replaceable>name</replaceable> is the provided column name. + </para> + <para> + Optionally, you can add <literal>ON ERROR</literal> clause to define + error behavior. This clause has the same syntax and semantics as + for <function>json_exists</function>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>NESTED PATH</literal> <replaceable>json_path_specification</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> + <literal>COLUMNS</literal> ( <replaceable>json_table_column</replaceable> <optional>, ...</optional> ) + </term> + <listitem> + + <para> + Extracts SQL/JSON items from nested levels of the row pattern, + generates one or more columns as defined by the <literal>COLUMNS</literal> + subclause, and inserts the extracted SQL/JSON items into each row of these columns. + The <replaceable>json_table_column</replaceable> expression in the + <literal>COLUMNS</literal> subclause uses the same syntax as in the + parent <literal>COLUMNS</literal> clause. + </para> + + <para> + The <literal>NESTED PATH</literal> syntax is recursive, + so you can go down multiple nested levels by specifying several + <literal>NESTED PATH</literal> subclauses within each other. + It allows to unnest the hierarchy of JSON objects and arrays + in a single function invocation rather than chaining several + <function>JSON_TABLE</function> expressions in an SQL statement. + </para> + + <para> + You can use the <literal>PLAN</literal> clause to define how + to join the columns returned by <literal>NESTED PATH</literal> clauses. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <replaceable>name</replaceable> <literal>FOR ORDINALITY</literal> + </term> + <listitem> + + <para> + Adds an ordinality column that provides sequential row numbering. + You can have only one ordinality column per table. Row numbering + is 1-based. For child rows that result from the <literal>NESTED PATH</literal> + clauses, the parent row number is repeated. + </para> + </listitem> + </varlistentry> + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>AS</literal> <replaceable>json_path_name</replaceable> + </term> + <listitem> + + <para> + The optional <replaceable>json_path_name</replaceable> serves as an + identifier of the provided <replaceable>json_path_specification</replaceable>. + The path name must be unique and distinct from the column names. + When using the <literal>PLAN</literal> clause, you must specify the names + for all the paths, including the row pattern. Each path name can appear in + the <literal>PLAN</literal> clause only once. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN</literal> ( <replaceable>json_table_plan</replaceable> ) + </term> + <listitem> + + <para> + Defines how to join the data returned by <literal>NESTED PATH</literal> + clauses to the constructed view. + </para> + <para> + To join columns with parent/child relationship, you can use: + </para> + <variablelist> + <varlistentry> + <term> + <literal>INNER</literal> + </term> + <listitem> + + <para> + Use <literal>INNER JOIN</literal>, so that the parent row + is omitted from the output if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>OUTER</literal> + </term> + <listitem> + + <para> + Use <literal>LEFT OUTER JOIN</literal>, so that the parent row + is always included into the output even if it does not have any child rows + after joining the data returned by <literal>NESTED PATH</literal>, with NULL values + inserted into the child columns if the corresponding + values are missing. + </para> + <para> + This is the default option for joining columns with parent/child relationship. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para> + To join sibling columns, you can use: + </para> + + <variablelist> + <varlistentry> + <term> + <literal>UNION</literal> + </term> + <listitem> + + <para> + Generate one row for each value produced by each of the sibling + columns. The columns from the other siblings are set to null. + </para> + <para> + This is the default option for joining sibling columns. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>CROSS</literal> + </term> + <listitem> + + <para> + Generate one row for each combination of values from the sibling columns. + </para> + </listitem> + </varlistentry> + + </variablelist> + + </listitem> + </varlistentry> + + <varlistentry> + <term> + <literal>PLAN DEFAULT</literal> ( <literal><replaceable>OUTER | INNER</replaceable> <optional>, <replaceable>UNION | CROSS</replaceable> </optional></literal> ) + </term> + <listitem> + <para> + The terms can also be specified in reverse order. The + <literal>INNER</literal> or <literal>OUTER</literal> option defines the + joining plan for parent/child columns, while <literal>UNION</literal> or + <literal>CROSS</literal> affects joins of sibling columns. This form + of <literal>PLAN</literal> overrides the default plan for + all columns at once. Even though the path names are not included in the + <literal>PLAN DEFAULT</literal> form, to conform to the SQL/JSON standard + they must be provided for all the paths if the <literal>PLAN</literal> + clause is used. + </para> + <para> + <literal>PLAN DEFAULT</literal> is simpler than specifying a complete + <literal>PLAN</literal>, and is often all that is required to get the desired + output. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para>Examples</para> + + <para> + In these examples the following small table storing some JSON data will be used: +<programlisting> +CREATE TABLE my_films ( js jsonb ); + +INSERT INTO my_films VALUES ( +'{ "favorites" : [ + { "kind" : "comedy", "films" : [ + { "title" : "Bananas", + "director" : "Woody Allen"}, + { "title" : "The Dinner Game", + "director" : "Francis Veber" } ] }, + { "kind" : "horror", "films" : [ + { "title" : "Psycho", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "thriller", "films" : [ + { "title" : "Vertigo", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "drama", "films" : [ + { "title" : "Yojimbo", + "director" : "Akira Kurosawa" } ] } + ] }'); +</programlisting> + </para> + <para> + Query the <structname>my_films</structname> table holding + some JSON data about the films and create a view that + distributes the film genre, title, and director between separate columns: +<screen> +SELECT jt.* FROM + my_films, + JSON_TABLE ( js, '$.favorites[*]' COLUMNS ( + id FOR ORDINALITY, + kind text PATH '$.kind', + NESTED PATH '$.films[*]' COLUMNS ( + title text PATH '$.title', + director text PATH '$.director'))) AS jt; +----+----------+------------------+------------------- + id | kind | title | director +----+----------+------------------+------------------- + 1 | comedy | Bananas | Woody Allen + 1 | comedy | The Dinner Game | Francis Veber + 2 | horror | Psycho | Alfred Hitchcock + 3 | thriller | Vertigo | Alfred Hitchcock + 4 | drama | Yojimbo | Akira Kurosawa + (5 rows) +</screen> + </para> + + <para> + Find a director that has done films in two different genres: +<screen> +SELECT + director1 AS director, title1, kind1, title2, kind2 +FROM + my_films, + JSON_TABLE ( js, '$.favorites' AS favs COLUMNS ( + NESTED PATH '$[*]' AS films1 COLUMNS ( + kind1 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film1 COLUMNS ( + title1 text PATH '$.title', + director1 text PATH '$.director') + ), + NESTED PATH '$[*]' AS films2 COLUMNS ( + kind2 text PATH '$.kind', + NESTED PATH '$.films[*]' AS film2 COLUMNS ( + title2 text PATH '$.title', + director2 text PATH '$.director' + ) + ) + ) + PLAN (favs OUTER ((films1 INNER film1) CROSS (films2 INNER film2))) + ) AS jt + WHERE kind1 > kind2 AND director1 = director2; + + director | title1 | kind1 | title2 | kind2 +------------------+---------+----------+--------+-------- + Alfred Hitchcock | Vertigo | thriller | Psycho | horror +(1 row) +</screen> + </para> + </sect2> + <sect2 id="functions-sqljson-path"> <title>The SQL/JSON Path Language</title> diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 8570b14f62..ad899de5d6 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -3862,7 +3862,13 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es) break; case T_TableFuncScan: Assert(rte->rtekind == RTE_TABLEFUNC); - objectname = "xmltable"; + if (rte->tablefunc) + if (rte->tablefunc->functype == TFT_XMLTABLE) + objectname = "xmltable"; + else /* Must be TFT_JSON_TABLE */ + objectname = "json_table"; + else + objectname = NULL; objecttag = "Table Function Name"; break; case T_ValuesScan: diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index cf42d45c98..29e2d6500b 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -4308,6 +4308,11 @@ ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) break; } + case JSON_TABLE_OP: + res = item; + resnull = false; + break; + default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); *op->resnull = true; diff --git a/src/backend/executor/nodeTableFuncscan.c b/src/backend/executor/nodeTableFuncscan.c index 791cbd2372..085a612533 100644 --- a/src/backend/executor/nodeTableFuncscan.c +++ b/src/backend/executor/nodeTableFuncscan.c @@ -28,6 +28,7 @@ #include "miscadmin.h" #include "nodes/execnodes.h" #include "utils/builtins.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/xml.h" @@ -161,8 +162,9 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) scanstate->ss.ps.qual = ExecInitQual(node->scan.plan.qual, &scanstate->ss.ps); - /* Only XMLTABLE is supported currently */ - scanstate->routine = &XmlTableRoutine; + /* Only XMLTABLE and JSON_TABLE are supported currently */ + scanstate->routine = + tf->functype == TFT_XMLTABLE ? &XmlTableRoutine : &JsonbTableRoutine; scanstate->perTableCxt = AllocSetContextCreate(CurrentMemoryContext, @@ -182,6 +184,10 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) ExecInitExprList(tf->colexprs, (PlanState *) scanstate); scanstate->coldefexprs = ExecInitExprList(tf->coldefexprs, (PlanState *) scanstate); + scanstate->colvalexprs = + ExecInitExprList(tf->colvalexprs, (PlanState *) scanstate); + scanstate->passingvalexprs = + ExecInitExprList(tf->passingvalexprs, (PlanState *) scanstate); scanstate->notnulls = tf->notnulls; @@ -381,14 +387,17 @@ tfuncInitialize(TableFuncScanState *tstate, ExprContext *econtext, Datum doc) routine->SetNamespace(tstate, ns_name, ns_uri); } - /* Install the row filter expression into the table builder context */ - value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); - if (isnull) - ereport(ERROR, - (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), - errmsg("row filter expression must not be null"))); + if (routine->SetRowFilter) + { + /* Install the row filter expression into the table builder context */ + value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); + if (isnull) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("row filter expression must not be null"))); - routine->SetRowFilter(tstate, TextDatumGetCString(value)); + routine->SetRowFilter(tstate, TextDatumGetCString(value)); + } /* * Install the column filter expressions into the table builder context. diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 9c02634355..dd5e17c6e9 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -875,6 +875,25 @@ makeJsonBehavior(JsonBehaviorType type, Node *default_expr) return behavior; } +/* + * makeJsonTableJoinedPlan - + * creates a joined JsonTablePlan node + */ +Node * +makeJsonTableJoinedPlan(JsonTablePlanJoinType type, Node *plan1, Node *plan2, + int location) +{ + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_JOINED; + n->join_type = type; + n->plan1 = castNode(JsonTablePlan, plan1); + n->plan2 = castNode(JsonTablePlan, plan2); + n->location = location; + + return (Node *) n; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index cd5fff2bbe..64a4fbf093 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -2629,6 +2629,10 @@ expression_tree_walker_impl(Node *node, return true; if (WALK(tf->coldefexprs)) return true; + if (WALK(tf->colvalexprs)) + return true; + if (WALK(tf->passingvalexprs)) + return true; } break; default: @@ -3697,6 +3701,8 @@ expression_tree_mutator_impl(Node *node, MUTATE(newnode->rowexpr, tf->rowexpr, Node *); MUTATE(newnode->colexprs, tf->colexprs, List *); MUTATE(newnode->coldefexprs, tf->coldefexprs, List *); + MUTATE(newnode->colvalexprs, tf->colvalexprs, List *); + MUTATE(newnode->passingvalexprs, tf->passingvalexprs, List *); return (Node *) newnode; } break; @@ -4128,6 +4134,30 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_JsonTable: + { + JsonTable *jt = (JsonTable *) node; + + if (WALK(jt->common)) + return true; + if (WALK(jt->columns)) + return true; + } + break; + case T_JsonTableColumn: + { + JsonTableColumn *jtc = (JsonTableColumn *) node; + + if (WALK(jtc->typeName)) + return true; + if (WALK(jtc->on_empty)) + return true; + if (WALK(jtc->on_error)) + return true; + if (jtc->coltype == JTC_NESTED && WALK(jtc->columns)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile index 9f1c4022bb..f4c0cc7f10 100644 --- a/src/backend/parser/Makefile +++ b/src/backend/parser/Makefile @@ -23,6 +23,7 @@ OBJS = \ parse_enr.o \ parse_expr.o \ parse_func.o \ + parse_jsontable.o \ parse_merge.o \ parse_node.o \ parse_oper.o \ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index b867eddfff..11e78ac61b 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -654,19 +654,43 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_aggregate_func json_api_common_syntax json_argument + json_table + json_table_column_definition + json_table_ordinality_column_definition + json_table_regular_column_definition + json_table_formatted_column_definition + json_table_exists_column_definition + json_table_nested_columns + json_table_plan_clause_opt + json_table_plan + json_table_plan_simple + json_table_plan_parent_child + json_table_plan_outer + json_table_plan_inner + json_table_plan_sibling + json_table_plan_union + json_table_plan_cross + json_table_plan_primary %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt json_arguments + json_table_columns_clause + json_table_column_definition_list +%type <str> json_table_column_path_specification_clause_opt %type <ival> json_encoding_clause_opt json_predicate_type_constraint json_wrapper_behavior + json_table_default_plan_choices + json_table_default_plan_inner_outer + json_table_default_plan_union_cross %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt %type <jsbehavior> json_value_behavior json_query_behavior json_exists_behavior + json_table_behavior %type <js_quotes> json_quotes_clause_opt @@ -733,7 +757,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG - JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_TABLE JSON_VALUE KEY KEYS KEEP @@ -744,8 +768,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE - NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE - NORMALIZE NORMALIZED + NAME_P NAMES NATIONAL NATURAL NCHAR NESTED NEW NEXT NFC NFD NFKC NFKD NO + NONE NORMALIZE NORMALIZED NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC @@ -753,8 +777,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER - PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD - PLACING PLANS POLICY + PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH + PLACING PLAN PLANS POLICY POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION @@ -862,6 +886,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); * Using the same precedence as IDENT seems right for the reasons given above. */ %nonassoc UNBOUNDED /* ideally would have same precedence as IDENT */ +%nonassoc COLUMNS %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' @@ -884,6 +909,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL +%nonassoc json_table_column +%nonassoc NESTED +%left PATH %% /* @@ -13348,6 +13376,21 @@ table_ref: relation_expr opt_alias_clause $2->alias = $4; $$ = (Node *) $2; } + | json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $1); + + jt->alias = $2; + $$ = (Node *) jt; + } + | LATERAL_P json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $2); + + jt->alias = $3; + jt->lateral = true; + $$ = (Node *) jt; + } ; @@ -13915,6 +13958,8 @@ xmltable_column_option_el: { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(true), @1); } | NULL_P { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(false), @1); } + | PATH b_expr + { $$ = makeDefElem("path", $2, @1); } ; xml_namespace_list: @@ -16722,6 +16767,11 @@ json_value_behavior: | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } ; +json_table_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + ; + /* ARRAY is a noise word */ json_wrapper_behavior: WITHOUT WRAPPER { $$ = JSW_NONE; } @@ -16743,6 +16793,414 @@ json_quotes_clause_opt: | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } ; +json_table: + JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_plan_clause_opt + json_table_behavior ON ERROR_P + ')' + { + JsonTable *n = makeNode(JsonTable); + + n->common = (JsonCommon *) $3; + n->columns = $4; + n->plan = (JsonTablePlan *) $5; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_columns_clause: + COLUMNS '(' json_table_column_definition_list ')' { $$ = $3; } + ; + +json_table_column_definition_list: + json_table_column_definition + { $$ = list_make1($1); } + | json_table_column_definition_list ',' json_table_column_definition + { $$ = lappend($1, $3); } + ; + +json_table_column_definition: + json_table_ordinality_column_definition %prec json_table_column + | json_table_regular_column_definition %prec json_table_column + | json_table_formatted_column_definition %prec json_table_column + | json_table_exists_column_definition %prec json_table_column + | json_table_nested_columns + ; + +json_table_ordinality_column_definition: + ColId FOR ORDINALITY + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FOR_ORDINALITY; + n->name = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_regular_column_definition: + ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* disallowed during parse-analysis! */ + n->quotes = $5; /* disallowed during parse-analysis! */ + n->pathspec = $3; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* disallowed during parse-analysis! */ + n->quotes = $5; /* disallowed during parse-analysis! */ + n->pathspec = $3; + n->on_empty = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* disallowed during parse-analysis! */ + n->quotes = $5; /* disallowed during parse-analysis! */ + n->pathspec = $3; + n->on_error = $6; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* disallowed during parse-analysis! */ + n->quotes = $5; /* disallowed during parse-analysis! */ + n->pathspec = $3; + n->on_empty = $6; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_formatted_column_definition: + ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->quotes = $8; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->quotes = $8; + n->on_empty = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->quotes = $8; + n->on_error = $9; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename FORMAT_LA JSON json_encoding_clause_opt + json_table_column_path_specification_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1); + n->pathspec = $6; + n->wrapper = $7; + if (n->wrapper != JSW_NONE && $8 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->quotes = $8; + n->on_empty = $9; + n->on_error = $12; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_exists_column_definition: + ColId Typename + EXISTS json_table_column_path_specification_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->quotes = JS_QUOTES_UNSPEC; + n->pathspec = $4; + n->location = @1; + $$ = (Node *) n; + } + | ColId Typename + EXISTS json_table_column_path_specification_clause_opt + json_exists_behavior ON ERROR_P + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->quotes = JS_QUOTES_UNSPEC; + n->pathspec = $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_column_path_specification_clause_opt: + PATH Sconst { $$ = $2; } + | /* EMPTY */ %prec json_table_column { $$ = NULL; } + ; + +json_table_nested_columns: + NESTED path_opt Sconst + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = NULL; + n->columns = $4; + n->location = @1; + $$ = (Node *) n; + } + | NESTED path_opt Sconst AS name + json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->pathname = $5; + n->columns = $6; + n->location = @1; + $$ = (Node *) n; + } + ; + +path_opt: + PATH { } + | /* EMPTY */ { } + ; + +json_table_plan_clause_opt: + PLAN '(' json_table_plan ')' { $$ = $3; } + | PLAN DEFAULT '(' json_table_default_plan_choices ')' + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_DEFAULT; + n->join_type = $4; + n->location = @1; + $$ = (Node *) n; + } + | /* EMPTY */ { $$ = NULL; } + ; + +json_table_plan: + json_table_plan_simple + | json_table_plan_parent_child + | json_table_plan_sibling + ; + +json_table_plan_simple: + name + { + JsonTablePlan *n = makeNode(JsonTablePlan); + + n->plan_type = JSTP_SIMPLE; + n->pathname = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_plan_primary: + json_table_plan_simple { $$ = $1; } + | '(' json_table_plan ')' + { + castNode(JsonTablePlan, $2)->location = @1; + $$ = $2; + } + ; + +json_table_plan_outer: + json_table_plan_simple OUTER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_OUTER, $1, $3, @1); } + ; + +json_table_plan_inner: + json_table_plan_simple INNER_P json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_INNER, $1, $3, @1); } + ; + +json_table_plan_parent_child: + json_table_plan_outer + | json_table_plan_inner + ; + +json_table_plan_union: + json_table_plan_primary UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + | json_table_plan_union UNION json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); } + ; + +json_table_plan_cross: + json_table_plan_primary CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + | json_table_plan_cross CROSS json_table_plan_primary + { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); } + ; + +json_table_plan_sibling: + json_table_plan_union + | json_table_plan_cross + ; + +json_table_default_plan_choices: + json_table_default_plan_inner_outer { $$ = $1 | JSTPJ_UNION; } + | json_table_default_plan_union_cross { $$ = $1 | JSTPJ_OUTER; } + | json_table_default_plan_inner_outer ',' + json_table_default_plan_union_cross { $$ = $1 | $3; } + | json_table_default_plan_union_cross ',' + json_table_default_plan_inner_outer { $$ = $1 | $3; } + ; + +json_table_default_plan_inner_outer: + INNER_P { $$ = JSTPJ_INNER; } + | OUTER_P { $$ = JSTPJ_OUTER; } + ; + +json_table_default_plan_union_cross: + UNION { $$ = JSTPJ_UNION; } + | CROSS { $$ = JSTPJ_CROSS; } + ; + json_returning_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17467,6 +17925,7 @@ unreserved_keyword: | MOVE | NAME_P | NAMES + | NESTED | NEW | NEXT | NFC @@ -17501,6 +17960,8 @@ unreserved_keyword: | PARTITION | PASSING | PASSWORD + | PATH + | PLAN | PLANS | POLICY | PRECEDING @@ -17665,6 +18126,7 @@ col_name_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | LEAST | NATIONAL @@ -18033,6 +18495,7 @@ bare_label_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | KEEP | KEY @@ -18072,6 +18535,7 @@ bare_label_keyword: | NATIONAL | NATURAL | NCHAR + | NESTED | NEW | NEXT | NFC @@ -18116,7 +18580,9 @@ bare_label_keyword: | PARTITION | PASSING | PASSWORD + | PATH | PLACING + | PLAN | PLANS | POLICY | POSITION diff --git a/src/backend/parser/meson.build b/src/backend/parser/meson.build index 94517a7f7d..0f0e4ac7ca 100644 --- a/src/backend/parser/meson.build +++ b/src/backend/parser/meson.build @@ -10,6 +10,7 @@ backend_sources += files( 'parse_enr.c', 'parse_expr.c', 'parse_func.c', + 'parse_jsontable.c', 'parse_merge.c', 'parse_node.c', 'parse_oper.c', diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index 334b9b42bd..affd812619 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -697,7 +697,11 @@ transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf) char **names; int colno; - /* Currently only XMLTABLE is supported */ + /* + * Currently we only support XMLTABLE here. See transformJsonTable() for + * JSON_TABLE support. + */ + tf->functype = TFT_XMLTABLE; constructName = "XMLTABLE"; docType = XMLOID; @@ -1104,13 +1108,17 @@ transformFromClauseItem(ParseState *pstate, Node *n, rtr->rtindex = nsitem->p_rtindex; return (Node *) rtr; } - else if (IsA(n, RangeTableFunc)) + else if (IsA(n, RangeTableFunc) || IsA(n, JsonTable)) { /* table function is like a plain relation */ RangeTblRef *rtr; ParseNamespaceItem *nsitem; - nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + if (IsA(n, RangeTableFunc)) + nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + else + nsitem = transformJsonTable(pstate, (JsonTable *) n); + *top_nsitem = nsitem; *namespace = list_make1(nsitem); rtr = makeNode(RangeTblRef); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 180188f4aa..f4209a67d6 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4250,7 +4250,7 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) JsonFormatType format; char *constructName; - if (func->common->pathname) + if (func->common->pathname && func->op != JSON_TABLE_OP) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("JSON_TABLE path name is not allowed here"), @@ -4269,6 +4269,9 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) case JSON_VALUE_OP: constructName = "JSON_VALUE()"; break; + case JSON_TABLE_OP: + constructName = "JSON_TABLE()"; + break; default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); break; @@ -4308,13 +4311,16 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) &jsexpr->passing_values, &jsexpr->passing_names); - if (func->op != JSON_EXISTS_OP) + if (func->op != JSON_EXISTS_OP && func->op != JSON_TABLE_OP) jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, JSON_BEHAVIOR_NULL); if (func->op == JSON_EXISTS_OP) jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, JSON_BEHAVIOR_FALSE); + else if (func->op == JSON_TABLE_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_EMPTY); else jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, JSON_BEHAVIOR_NULL); @@ -4638,7 +4644,15 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->coercions = makeNode(JsonItemCoercions); initJsonItemCoercions(pstate, jsexpr->coercions, jsexpr->returning, exprType(contextItemExpr)); + break; + + case JSON_TABLE_OP: + func_name = "JSON_TABLE"; + jsexpr->returning = makeNode(JsonReturning); + jsexpr->returning->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + jsexpr->returning->typid = exprType(contextItemExpr); + jsexpr->returning->typmod = -1; break; } diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c new file mode 100644 index 0000000000..ae10f2384c --- /dev/null +++ b/src/backend/parser/parse_jsontable.c @@ -0,0 +1,751 @@ +/*------------------------------------------------------------------------- + * + * parse_jsontable.c + * parsing of JSON_TABLE + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/parser/parse_jsontable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/pg_collation.h" +#include "catalog/pg_type.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "optimizer/optimizer.h" +#include "parser/parse_clause.h" +#include "parser/parse_collate.h" +#include "parser/parse_expr.h" +#include "parser/parse_relation.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/json.h" +#include "utils/lsyscache.h" + +/* Context for JSON_TABLE transformation */ +typedef struct JsonTableParseContext +{ + ParseState *pstate; /* parsing state */ + JsonTable *table; /* untransformed node */ + TableFunc *tablefunc; /* transformed node */ + List *pathNames; /* list of all path and columns names */ + int pathNameId; /* path name id counter */ + Oid contextItemTypid; /* type oid of context item (json/jsonb) */ +} JsonTableParseContext; + +static JsonTableParent *transformJsonTableColumns(JsonTableParseContext *cxt, + JsonTablePlan *plan, + List *columns, + char *pathSpec, + char **pathName, + int location); + +static Node * +makeStringConst(char *str, int location) +{ + A_Const *n = makeNode(A_Const); + + n->val.node.type = T_String; + n->val.sval.sval = str; + n->location = location; + + return (Node *) n; +} + +/* + * Transform JSON_TABLE column + * - regular column into JSON_VALUE() + * - FORMAT JSON column into JSON_QUERY() + * - EXISTS column into JSON_EXISTS() + */ +static Node * +transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, + List *passingArgs, bool errorOnError) +{ + JsonFuncExpr *jfexpr = makeNode(JsonFuncExpr); + JsonCommon *common = makeNode(JsonCommon); + JsonOutput *output = makeNode(JsonOutput); + char *pathspec; + JsonFormat *default_format; + + jfexpr->op = + jtc->coltype == JTC_REGULAR ? JSON_VALUE_OP : + jtc->coltype == JTC_EXISTS ? JSON_EXISTS_OP : JSON_QUERY_OP; + jfexpr->common = common; + jfexpr->output = output; + jfexpr->on_empty = jtc->on_empty; + jfexpr->on_error = jtc->on_error; + if (!jfexpr->on_error && errorOnError) + jfexpr->on_error = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); + jfexpr->quotes = jtc->quotes; + jfexpr->wrapper = jtc->wrapper; + jfexpr->location = jtc->location; + + output->typeName = jtc->typeName; + output->returning = makeNode(JsonReturning); + output->returning->format = jtc->format; + + default_format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + + common->pathname = NULL; + /* formatted_expr will be set later in transformJsonValueExpr(). */ + common->expr = makeJsonValueExpr((Expr *) contextItemExpr, NULL, + default_format); + common->passing = passingArgs; + + if (jtc->pathspec) + pathspec = jtc->pathspec; + else + { + /* Construct default path as '$."column_name"' */ + StringInfoData path; + + initStringInfo(&path); + + appendStringInfoString(&path, "$."); + escape_json(&path, jtc->name); + + pathspec = path.data; + } + + common->pathspec = makeStringConst(pathspec, -1); + + return (Node *) jfexpr; +} + +static bool +isJsonTablePathNameDuplicate(JsonTableParseContext *cxt, const char *pathname) +{ + ListCell *lc; + + foreach(lc, cxt->pathNames) + { + if (!strcmp(pathname, (const char *) lfirst(lc))) + return true; + } + + return false; +} + +/* Register the column name in the path name list. */ +static void +registerJsonTableColumn(JsonTableParseContext *cxt, char *colname) +{ + if (isJsonTablePathNameDuplicate(cxt, colname)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_ALIAS), + errmsg("duplicate JSON_TABLE column name: %s", colname), + errhint("JSON_TABLE column names must be distinct from one another."))); + + cxt->pathNames = lappend(cxt->pathNames, colname); +} + +/* Recursively register all nested column names in the path name list. */ +static void +registerAllJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED) + { + if (jtc->pathname) + registerJsonTableColumn(cxt, jtc->pathname); + + registerAllJsonTableColumns(cxt, jtc->columns); + } + else + { + registerJsonTableColumn(cxt, jtc->name); + } + } +} + +/* Generate a new unique JSON_TABLE path name. */ +static char * +generateJsonTablePathName(JsonTableParseContext *cxt) +{ + char namebuf[32]; + char *name = namebuf; + + do + { + snprintf(namebuf, sizeof(namebuf), "json_table_path_%d", + ++cxt->pathNameId); + } while (isJsonTablePathNameDuplicate(cxt, name)); + + name = pstrdup(name); + cxt->pathNames = lappend(cxt->pathNames, name); + + return name; +} + +/* Collect sibling path names from plan to the specified list. */ +static void +collectSiblingPathsInJsonTablePlan(JsonTablePlan *plan, List **paths) +{ + if (plan->plan_type == JSTP_SIMPLE) + *paths = lappend(*paths, plan->pathname); + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + *paths = lappend(*paths, plan->plan1->pathname); + } + else if (plan->join_type == JSTPJ_CROSS || + plan->join_type == JSTPJ_UNION) + { + collectSiblingPathsInJsonTablePlan(plan->plan1, paths); + collectSiblingPathsInJsonTablePlan(plan->plan2, paths); + } + else + elog(ERROR, "invalid JSON_TABLE join type %d", + plan->join_type); + } +} + +/* + * Validate child JSON_TABLE plan by checking that: + * - all nested columns have path names specified + * - all nested columns have corresponding node in the sibling plan + * - plan does not contain duplicate or extra nodes + */ +static void +validateJsonTableChildPlan(ParseState *pstate, JsonTablePlan *plan, + List *columns) +{ + ListCell *lc1; + List *siblings = NIL; + int nchildren = 0; + + if (plan) + collectSiblingPathsInJsonTablePlan(plan, &siblings); + + foreach(lc1, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc1)); + + if (jtc->coltype == JTC_NESTED) + { + ListCell *lc2; + bool found = false; + + if (!jtc->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("nested JSON_TABLE columns must contain an explicit AS pathname specification if an explicit PLAN clause is used"), + parser_errposition(pstate, jtc->location))); + + /* find nested path name in the list of sibling path names */ + foreach(lc2, siblings) + { + if ((found = !strcmp(jtc->pathname, lfirst(lc2)))) + break; + } + + if (!found) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node for nested path %s was not found in plan.", jtc->pathname), + parser_errposition(pstate, jtc->location))); + + nchildren++; + } + } + + if (list_length(siblings) > nchildren) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Plan node contains some extra or duplicate sibling nodes."), + parser_errposition(pstate, plan ? plan->location : -1))); +} + +static JsonTableColumn * +findNestedJsonTableColumn(List *columns, const char *pathname) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED && + jtc->pathname && + !strcmp(jtc->pathname, pathname)) + return jtc; + } + + return NULL; +} + +static Node * +transformNestedJsonTableColumn(JsonTableParseContext *cxt, JsonTableColumn *jtc, + JsonTablePlan *plan) +{ + JsonTableParent *node; + char *pathname = jtc->pathname; + + node = transformJsonTableColumns(cxt, plan, jtc->columns, jtc->pathspec, + &pathname, jtc->location); + + return (Node *) node; +} + +static Node * +makeJsonTableSiblingJoin(bool cross, Node *lnode, Node *rnode) +{ + JsonTableSibling *join = makeNode(JsonTableSibling); + + join->larg = lnode; + join->rarg = rnode; + join->cross = cross; + + return (Node *) join; +} + +/* + * Recursively transform child JSON_TABLE plan. + * + * Default plan is transformed into a cross/union join of its nested columns. + * Simple and outer/inner plans are transformed into a JsonTableParent by + * finding and transforming corresponding nested column. + * Sibling plans are recursively transformed into a JsonTableSibling. + */ +static Node * +transformJsonTableChildPlan(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns) +{ + JsonTableColumn *jtc = NULL; + + if (!plan || plan->plan_type == JSTP_DEFAULT) + { + /* unspecified or default plan */ + Node *res = NULL; + ListCell *lc; + bool cross = plan && (plan->join_type & JSTPJ_CROSS); + + /* transform all nested columns into cross/union join */ + foreach(lc, columns) + { + JsonTableColumn *col = castNode(JsonTableColumn, lfirst(lc)); + Node *node; + + if (col->coltype != JTC_NESTED) + continue; + + node = transformNestedJsonTableColumn(cxt, col, plan); + + /* join transformed node with previous sibling nodes */ + res = res ? makeJsonTableSiblingJoin(cross, res, node) : node; + } + + return res; + } + else if (plan->plan_type == JSTP_SIMPLE) + { + jtc = findNestedJsonTableColumn(columns, plan->pathname); + } + else if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type == JSTPJ_INNER || + plan->join_type == JSTPJ_OUTER) + { + Assert(plan->plan1->plan_type == JSTP_SIMPLE); + jtc = findNestedJsonTableColumn(columns, plan->plan1->pathname); + } + else + { + Node *node1 = transformJsonTableChildPlan(cxt, plan->plan1, + columns); + Node *node2 = transformJsonTableChildPlan(cxt, plan->plan2, + columns); + + return makeJsonTableSiblingJoin(plan->join_type == JSTPJ_CROSS, + node1, node2); + } + } + else + elog(ERROR, "invalid JSON_TABLE plan type %d", plan->plan_type); + + if (!jtc) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name was %s not found in nested columns list.", + plan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + return transformNestedJsonTableColumn(cxt, jtc, plan); +} + +/* Check whether type is json/jsonb, array, or record. */ +static bool +typeIsComposite(Oid typid) +{ + char typtype; + + if (typid == JSONOID || + typid == JSONBOID || + typid == RECORDOID || + type_is_array(typid)) + return true; + + typtype = get_typtype(typid); + + if (typtype == TYPTYPE_COMPOSITE) + return true; + + if (typtype == TYPTYPE_DOMAIN) + return typeIsComposite(getBaseType(typid)); + + return false; +} + +/* Append transformed non-nested JSON_TABLE columns to the TableFunc node */ +static void +appendJsonTableColumns(JsonTableParseContext *cxt, List *columns) +{ + ListCell *col; + ParseState *pstate = cxt->pstate; + JsonTable *jt = cxt->table; + TableFunc *tf = cxt->tablefunc; + bool ordinality_found = false; + bool errorOnError = jt->on_error && + jt->on_error->btype == JSON_BEHAVIOR_ERROR; + + foreach(col, columns) + { + JsonTableColumn *rawc = castNode(JsonTableColumn, lfirst(col)); + Oid typid; + int32 typmod; + Node *colexpr; + + if (rawc->name) + { + /* make sure column names are unique */ + ListCell *colname; + + foreach(colname, tf->colnames) + if (!strcmp((const char *) colname, rawc->name)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("column name \"%s\" is not unique", + rawc->name), + parser_errposition(pstate, rawc->location))); + + tf->colnames = lappend(tf->colnames, + makeString(pstrdup(rawc->name))); + } + + /* + * Determine the type and typmod for the new column. FOR ORDINALITY + * columns are INTEGER by standard; the others are user-specified. + */ + switch (rawc->coltype) + { + case JTC_FOR_ORDINALITY: + if (ordinality_found) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use more than one FOR ORDINALITY column"), + parser_errposition(pstate, rawc->location))); + ordinality_found = true; + colexpr = NULL; + typid = INT4OID; + typmod = -1; + break; + + case JTC_REGULAR: + typenameTypeIdAndMod(pstate, rawc->typeName, &typid, &typmod); + + /* + * Use implicit FORMAT JSON for composite types (arrays and + * records). + * + * For scalar columns, require an explicit FORMAT JSON clause + * to get the specified WRAPPER and QUOTES behavior. + */ + if (typeIsComposite(typid)) + rawc->coltype = JTC_FORMATTED; + else if (rawc->wrapper != JSW_NONE) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use WITH WRAPPER clause for scalar columns without also specifying FORMAT clause"), + parser_errposition(pstate, rawc->location))); + else if (rawc->quotes != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use OMIT/KEEP QUOTES clause for scalar columns without also specifying FORMAT clause"), + parser_errposition(pstate, rawc->location))); + + /* FALLTHROUGH */ + case JTC_EXISTS: + case JTC_FORMATTED: + { + Node *je; + CaseTestExpr *param = makeNode(CaseTestExpr); + + param->collation = InvalidOid; + param->typeId = cxt->contextItemTypid; + param->typeMod = -1; + + je = transformJsonTableColumn(rawc, (Node *) param, + NIL, errorOnError); + + colexpr = transformExpr(pstate, je, EXPR_KIND_FROM_FUNCTION); + assign_expr_collations(pstate, colexpr); + + typid = exprType(colexpr); + typmod = exprTypmod(colexpr); + break; + } + + case JTC_NESTED: + continue; + + default: + elog(ERROR, "unknown JSON_TABLE column type: %d", rawc->coltype); + break; + } + + tf->coltypes = lappend_oid(tf->coltypes, typid); + tf->coltypmods = lappend_int(tf->coltypmods, typmod); + tf->colcollations = lappend_oid(tf->colcollations, get_typcollation(typid)); + tf->colvalexprs = lappend(tf->colvalexprs, colexpr); + } +} + +/* + * Create transformed JSON_TABLE parent plan node by appending all non-nested + * columns to the TableFunc node and remembering their indices in the + * colvalexprs list. + */ +static JsonTableParent * +makeParentJsonTableNode(JsonTableParseContext *cxt, char *pathSpec, char *pathName, + List *columns) +{ + JsonTableParent *node = makeNode(JsonTableParent); + + node->path = makeNode(JsonTablePath); + node->path->value = makeConst(JSONPATHOID, -1, InvalidOid, -1, + DirectFunctionCall1(jsonpath_in, + CStringGetDatum(pathSpec)), + false, false); + if (pathName) + node->path->name = pstrdup(pathName); + + /* save start of column range */ + node->colMin = list_length(cxt->tablefunc->colvalexprs); + + appendJsonTableColumns(cxt, columns); + + /* save end of column range */ + node->colMax = list_length(cxt->tablefunc->colvalexprs) - 1; + + node->errorOnError = + cxt->table->on_error && + cxt->table->on_error->btype == JSON_BEHAVIOR_ERROR; + + return node; +} + +static JsonTableParent * +transformJsonTableColumns(JsonTableParseContext *cxt, JsonTablePlan *plan, + List *columns, char *pathSpec, char **pathName, + int location) +{ + JsonTableParent *node; + JsonTablePlan *childPlan; + bool defaultPlan = !plan || plan->plan_type == JSTP_DEFAULT; + + if (!*pathName) + { + if (cxt->table->plan) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE expression"), + errdetail("JSON_TABLE columns must contain " + "explicit AS pathname specification if " + "explicit PLAN clause is used"), + parser_errposition(cxt->pstate, location))); + + *pathName = generateJsonTablePathName(cxt); + } + + if (defaultPlan) + childPlan = plan; + else + { + /* validate parent and child plans */ + JsonTablePlan *parentPlan; + + if (plan->plan_type == JSTP_JOINED) + { + if (plan->join_type != JSTPJ_INNER && + plan->join_type != JSTPJ_OUTER) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Expected INNER or OUTER JSON_TABLE plan node."), + parser_errposition(cxt->pstate, plan->location))); + + parentPlan = plan->plan1; + childPlan = plan->plan2; + + Assert(parentPlan->plan_type != JSTP_JOINED); + Assert(parentPlan->pathname); + } + else + { + parentPlan = plan; + childPlan = NULL; + } + + if (strcmp(parentPlan->pathname, *pathName)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid JSON_TABLE plan"), + errdetail("Path name mismatch: expected %s but %s is given.", + *pathName, parentPlan->pathname), + parser_errposition(cxt->pstate, plan->location))); + + validateJsonTableChildPlan(cxt->pstate, childPlan, columns); + } + + /* transform only non-nested columns */ + node = makeParentJsonTableNode(cxt, pathSpec, *pathName, columns); + + if (childPlan || defaultPlan) + { + /* transform recursively nested columns */ + node->child = transformJsonTableChildPlan(cxt, childPlan, columns); + if (node->child) + node->outerJoin = !plan || (plan->join_type & JSTPJ_OUTER); + /* else: default plan case, no children found */ + } + + return node; +} + +/* + * transformJsonTable - + * Transform a raw JsonTable into TableFunc. + * + * Transform the document-generating expression, the row-generating expression, + * the column-generating expressions, and the default value expressions. + */ +ParseNamespaceItem * +transformJsonTable(ParseState *pstate, JsonTable *jt) +{ + JsonTableParseContext cxt; + TableFunc *tf = makeNode(TableFunc); + JsonFuncExpr *jfe = makeNode(JsonFuncExpr); + JsonExpr *je; + JsonTablePlan *plan = jt->plan; + JsonCommon *jscommon; + char *rootPathName = jt->common->pathname; + char *rootPath; + bool is_lateral; + + cxt.pstate = pstate; + cxt.table = jt; + cxt.tablefunc = tf; + cxt.pathNames = NIL; + cxt.pathNameId = 0; + + if (rootPathName) + registerJsonTableColumn(&cxt, rootPathName); + + registerAllJsonTableColumns(&cxt, jt->columns); + +#if 0 /* XXX it' unclear from the standard whether + * root path name is mandatory or not */ + if (plan && plan->plan_type != JSTP_DEFAULT && !rootPathName) + { + /* Assign root path name and create corresponding plan node */ + JsonTablePlan *rootNode = makeNode(JsonTablePlan); + JsonTablePlan *rootPlan = (JsonTablePlan *) + makeJsonTableJoinedPlan(JSTPJ_OUTER, (Node *) rootNode, + (Node *) plan, jt->location); + + rootPathName = generateJsonTablePathName(&cxt); + + rootNode->plan_type = JSTP_SIMPLE; + rootNode->pathname = rootPathName; + + plan = rootPlan; + } +#endif + + jscommon = copyObject(jt->common); + jscommon->pathspec = makeStringConst(pstrdup("$"), -1); + + jfe->op = JSON_TABLE_OP; + jfe->common = jscommon; + jfe->on_error = jt->on_error; + jfe->location = jt->common->location; + + /* + * We make lateral_only names of this level visible, whether or not the + * RangeTableFunc is explicitly marked LATERAL. This is needed for SQL + * spec compliance and seems useful on convenience grounds for all + * functions in FROM. + * + * (LATERAL can't nest within a single pstate level, so we don't need + * save/restore logic here.) + */ + Assert(!pstate->p_lateral_active); + pstate->p_lateral_active = true; + + tf->functype = TFT_JSON_TABLE; + tf->docexpr = transformExpr(pstate, (Node *) jfe, EXPR_KIND_FROM_FUNCTION); + + cxt.contextItemTypid = exprType(tf->docexpr); + + if (!IsA(jt->common->pathspec, A_Const) || + castNode(A_Const, jt->common->pathspec)->val.node.type != T_String) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("only string constants supported in JSON_TABLE path specification"), + parser_errposition(pstate, + exprLocation(jt->common->pathspec)))); + + rootPath = castNode(A_Const, jt->common->pathspec)->val.sval.sval; + + tf->plan = (Node *) transformJsonTableColumns(&cxt, plan, jt->columns, + rootPath, &rootPathName, + jt->common->location); + + /* Also save a copy of the PASSING arguments in the TableFunc node. */ + je = (JsonExpr *) tf->docexpr; + tf->passingvalexprs = copyObject(je->passing_values); + + tf->ordinalitycol = -1; /* undefine ordinality column number */ + tf->location = jt->location; + + pstate->p_lateral_active = false; + + /* + * Mark the RTE as LATERAL if the user said LATERAL explicitly, or if + * there are any lateral cross-references in it. + */ + is_lateral = jt->lateral || contain_vars_of_level((Node *) tf, 0); + + return addRangeTableEntryForTableFunc(pstate, + tf, jt->alias, is_lateral, true); +} diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 864ea9b0d5..3ac7e113f0 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -2073,7 +2073,8 @@ addRangeTableEntryForTableFunc(ParseState *pstate, Assert(list_length(tf->coltypmods) == list_length(tf->colnames)); Assert(list_length(tf->colcollations) == list_length(tf->colnames)); - refname = alias ? alias->aliasname : pstrdup("xmltable"); + refname = alias ? alias->aliasname : + pstrdup(tf->functype == TFT_XMLTABLE ? "xmltable" : "json_table"); rte->rtekind = RTE_TABLEFUNC; rte->relid = InvalidOid; @@ -2096,7 +2097,7 @@ addRangeTableEntryForTableFunc(ParseState *pstate, ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), errmsg("%s function has %d columns available but %d columns specified", - "XMLTABLE", + tf->functype == TFT_XMLTABLE ? "XMLTABLE" : "JSON_TABLE", list_length(tf->colnames), numaliases))); rte->eref = eref; diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 21bdb2815d..1d5f1f3fdd 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1995,6 +1995,9 @@ FigureColnameInternal(Node *node, char **name) case JSON_VALUE_OP: *name = "json_value"; return 2; + case JSON_TABLE_OP: + *name = "json_table"; + return 2; } break; default: diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index bf4a7220fd..f3d5eaa6fc 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -61,10 +61,12 @@ #include "catalog/pg_collation.h" #include "catalog/pg_type.h" +#include "executor/execExpr.h" #include "funcapi.h" #include "lib/stringinfo.h" #include "miscadmin.h" #include "nodes/miscnodes.h" +#include "nodes/nodeFuncs.h" #include "regex/regex.h" #include "utils/builtins.h" #include "utils/date.h" @@ -75,6 +77,8 @@ #include "utils/guc.h" #include "utils/json.h" #include "utils/jsonpath.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" #include "utils/timestamp.h" #include "utils/varlena.h" @@ -156,6 +160,61 @@ typedef struct JsonValueListIterator ListCell *next; } JsonValueListIterator; +/* Structures for JSON_TABLE execution */ + +typedef enum JsonTablePlanStateType +{ + JSON_TABLE_SCAN_STATE = 0, + JSON_TABLE_JOIN_STATE +} JsonTablePlanStateType; + +typedef struct JsonTablePlanState +{ + JsonTablePlanStateType type; + + struct JsonTablePlanState *parent; + struct JsonTablePlanState *nested; +} JsonTablePlanState; + +typedef struct JsonTableScanState +{ + JsonTablePlanState plan; + + MemoryContext mcxt; + JsonPath *path; + List *args; + JsonValueList found; + JsonValueListIterator iter; + Datum current; + int ordinal; + bool currentIsNull; + bool outerJoin; + bool errorOnError; + bool advanceNested; + bool reset; +} JsonTableScanState; + +typedef struct JsonTableJoinState +{ + JsonTablePlanState plan; + + JsonTablePlanState *left; + JsonTablePlanState *right; + bool cross; + bool advanceRight; +} JsonTableJoinState; + +/* random number to identify JsonTableExecContext */ +#define JSON_TABLE_EXEC_CONTEXT_MAGIC 418352867 + +typedef struct JsonTableExecContext +{ + int magic; + JsonTableScanState **colexprscans; + JsonTableScanState *root; + bool empty; +} JsonTableExecContext; + /* strict/lax flags is decomposed into four [un]wrap/error flags */ #define jspStrictAbsenseOfErrors(cxt) (!(cxt)->laxMode) #define jspAutoUnwrap(cxt) ((cxt)->laxMode) @@ -248,6 +307,7 @@ static JsonPathExecResult getArrayIndex(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbValue *jb, int32 *index); static JsonBaseObjectInfo setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id); +static void JsonValueListClear(JsonValueList *jvl); static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv); static int JsonValueListLength(const JsonValueList *jvl); static bool JsonValueListIsEmpty(JsonValueList *jvl); @@ -265,6 +325,14 @@ static JsonbValue *wrapItemsInArray(const JsonValueList *items); static int compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, bool useTz, bool *cast_error); + +static JsonTablePlanState *JsonTableInitPlanState(JsonTableExecContext *cxt, + Node *plan, + JsonTablePlanState *parent); +static bool JsonTablePlanNextRow(JsonTablePlanState *state); +static bool JsonTableScanNextRow(JsonTableScanState *scan); + + /****************** User interface to JsonPath executor ********************/ /* @@ -2518,6 +2586,13 @@ setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id) return baseObject; } +static void +JsonValueListClear(JsonValueList *jvl) +{ + jvl->singleton = NULL; + jvl->list = NULL; +} + static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv) { @@ -3123,3 +3198,471 @@ JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); } } + +/************************ JSON_TABLE functions ***************************/ + +/* + * Returns private data from executor state. Ensure validity by check with + * MAGIC number. + */ +static inline JsonTableExecContext * +GetJsonTableExecContext(TableFuncScanState *state, const char *fname) +{ + JsonTableExecContext *result; + + if (!IsA(state, TableFuncScanState)) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + result = (JsonTableExecContext *) state->opaque; + if (result->magic != JSON_TABLE_EXEC_CONTEXT_MAGIC) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + + return result; +} + +/* Recursively initialize JSON_TABLE scan / join state */ +static JsonTableJoinState * +JsonTableInitJoinState(JsonTableExecContext *cxt, JsonTableSibling *plan, + JsonTablePlanState *parent) +{ + JsonTableJoinState *join = palloc0(sizeof(*join)); + + join->plan.type = JSON_TABLE_JOIN_STATE; + /* parent and nested not set. */ + + join->cross = plan->cross; + join->left = JsonTableInitPlanState(cxt, plan->larg, parent); + join->right = JsonTableInitPlanState(cxt, plan->rarg, parent); + + return join; +} + +static JsonTableScanState * +JsonTableInitScanState(JsonTableExecContext *cxt, JsonTableParent *plan, + JsonTablePlanState *parent, + List *args, MemoryContext mcxt) +{ + JsonTableScanState *scan = palloc0(sizeof(*scan)); + int i; + + scan->plan.type = JSON_TABLE_SCAN_STATE; + scan->plan.parent = parent; + + scan->outerJoin = plan->outerJoin; + scan->errorOnError = plan->errorOnError; + scan->path = DatumGetJsonPathP(plan->path->value->constvalue); + scan->args = args; + scan->mcxt = AllocSetContextCreate(mcxt, "JsonTableExecContext", + ALLOCSET_DEFAULT_SIZES); + + /* + * Set after settings scan->args and scan->mcxt, because the recursive call + * wants to use those values. + */ + scan->plan.nested = plan->child ? + JsonTableInitPlanState(cxt, plan->child, (JsonTablePlanState *) scan) : + NULL; + + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + + for (i = plan->colMin; i <= plan->colMax; i++) + cxt->colexprscans[i] = scan; + + return scan; +} + +/* Recursively initialize JSON_TABLE scan state */ +static JsonTablePlanState * +JsonTableInitPlanState(JsonTableExecContext *cxt, Node *plan, + JsonTablePlanState *parent) +{ + JsonTablePlanState *state; + + if (IsA(plan, JsonTableSibling)) + { + JsonTableSibling *join = castNode(JsonTableSibling, plan); + + state = (JsonTablePlanState *) + JsonTableInitJoinState(cxt, join, parent); + } + else + { + JsonTableParent *scan = castNode(JsonTableParent, plan); + JsonTableScanState *parent_scan = (JsonTableScanState *) parent; + + Assert(parent_scan); + state = (JsonTablePlanState *) + JsonTableInitScanState(cxt, scan, parent, parent_scan->args, + parent_scan->mcxt); + } + + return state; +} + +/* + * JsonTableInitOpaque + * Fill in TableFuncScanState->opaque for JsonTable processor + */ +static void +JsonTableInitOpaque(TableFuncScanState *state, int natts) +{ + JsonTableExecContext *cxt; + PlanState *ps = &state->ss.ps; + TableFuncScan *tfs = castNode(TableFuncScan, ps->plan); + TableFunc *tf = tfs->tablefunc; + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + JsonExpr *je = castNode(JsonExpr, tf->docexpr); + List *args = NIL; + + cxt = palloc0(sizeof(JsonTableExecContext)); + cxt->magic = JSON_TABLE_EXEC_CONTEXT_MAGIC; + + if (state->passingvalexprs) + { + ListCell *exprlc; + ListCell *namelc; + + Assert(list_length(state->passingvalexprs) == + list_length(je->passing_names)); + forboth(exprlc, state->passingvalexprs, + namelc, je->passing_names) + { + ExprState *state = lfirst_node(ExprState, exprlc); + String *name = lfirst_node(String, namelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(name->sval); + var->typid = exprType((Node *) state->expr); + var->typmod = exprTypmod((Node *) state->expr); + + /* + * Evaluate the expression and save the value to be returned by + * GetJsonPathVar(). + */ + var->value = ExecEvalExpr(state, ps->ps_ExprContext, + &var->isnull); + + args = lappend(args, var); + } + } + + cxt->colexprscans = palloc(sizeof(JsonTableScanState *) * + list_length(tf->colvalexprs)); + + cxt->root = JsonTableInitScanState(cxt, root, NULL, args, + CurrentMemoryContext); + state->opaque = cxt; +} + +/* Reset scan iterator to the beginning of the item list */ +static void +JsonTableRescan(JsonTableScanState *scan) +{ + JsonValueListInitIterator(&scan->found, &scan->iter); + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + scan->advanceNested = false; + scan->ordinal = 0; +} + +/* Reset context item of a scan, execute JSON path and reset a scan */ +static void +JsonTableResetContextItem(JsonTableScanState *scan, Datum item) +{ + MemoryContext oldcxt; + JsonPathExecResult res; + Jsonb *js = (Jsonb *) DatumGetJsonbP(item); + + JsonValueListClear(&scan->found); + + MemoryContextResetOnly(scan->mcxt); + + oldcxt = MemoryContextSwitchTo(scan->mcxt); + + res = executeJsonPath(scan->path, scan->args, GetJsonPathVar, js, + scan->errorOnError, &scan->found, + false /* FIXME */ ); + + MemoryContextSwitchTo(oldcxt); + + if (jperIsError(res)) + { + Assert(!scan->errorOnError); + JsonValueListClear(&scan->found); /* EMPTY ON ERROR case */ + } + + JsonTableRescan(scan); +} + +/* + * JsonTableSetDocument + * Install the input document + */ +static void +JsonTableSetDocument(TableFuncScanState *state, Datum value) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableSetDocument"); + + JsonTableResetContextItem(cxt->root, value); +} + +/* Recursively reset scan and its child nodes */ +static void +JsonTableRescanRecursive(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTableRescanRecursive(join->left); + JsonTableRescanRecursive(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan = (JsonTableScanState *) state; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + JsonTableRescan(scan); + if (scan->plan.nested) + JsonTableRescanRecursive(scan->plan.nested); + } +} + +/* + * Fetch next row from a cross/union joined scan. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTablePlanNextRow(JsonTablePlanState *state) +{ + JsonTableJoinState *join; + + if (state->type == JSON_TABLE_SCAN_STATE) + return JsonTableScanNextRow((JsonTableScanState *) state); + + join = (JsonTableJoinState *) state; + if (join->advanceRight) + { + /* fetch next inner row */ + if (JsonTablePlanNextRow(join->right)) + return true; + + /* inner rows are exhausted */ + if (join->cross) + join->advanceRight = false; /* next outer row */ + else + return false; /* end of scan */ + } + + while (!join->advanceRight) + { + /* fetch next outer row */ + bool left = JsonTablePlanNextRow(join->left); + + if (join->cross) + { + if (!left) + return false; /* end of scan */ + + JsonTableRescanRecursive(join->right); + + if (!JsonTablePlanNextRow(join->right)) + continue; /* next outer row */ + + join->advanceRight = true; /* next inner row */ + } + else if (!left) + { + if (!JsonTablePlanNextRow(join->right)) + return false; /* end of scan */ + + join->advanceRight = true; /* next inner row */ + } + + break; + } + + return true; +} + +/* Recursively set 'reset' flag of scan and its child nodes */ +static void +JsonTablePlanReset(JsonTablePlanState *state) +{ + if (state->type == JSON_TABLE_JOIN_STATE) + { + JsonTableJoinState *join = (JsonTableJoinState *) state; + + JsonTablePlanReset(join->left); + JsonTablePlanReset(join->right); + join->advanceRight = false; + } + else + { + JsonTableScanState *scan; + + Assert(state->type == JSON_TABLE_SCAN_STATE); + scan = (JsonTableScanState *) state; + scan->reset = true; + scan->advanceNested = false; + + if (scan->plan.nested) + JsonTablePlanReset(scan->plan.nested); + } +} + +/* + * Fetch next row from a simple scan with outer/inner joined nested subscans. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTableScanNextRow(JsonTableScanState *scan) +{ + /* reset context item if requested */ + if (scan->reset) + { + JsonTableScanState *parent_scan = + (JsonTableScanState *) scan->plan.parent; + + Assert(parent_scan && !parent_scan->currentIsNull); + JsonTableResetContextItem(scan, parent_scan->current); + scan->reset = false; + } + + if (scan->advanceNested) + { + /* fetch next nested row */ + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested) + return true; + } + + for (;;) + { + /* fetch next row */ + JsonbValue *jbv = JsonValueListNext(&scan->found, &scan->iter); + MemoryContext oldcxt; + + if (!jbv) + { + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + return false; /* end of scan */ + } + + /* set current row item */ + oldcxt = MemoryContextSwitchTo(scan->mcxt); + scan->current = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + scan->currentIsNull = false; + MemoryContextSwitchTo(oldcxt); + + scan->ordinal++; + + if (!scan->plan.nested) + break; + + JsonTablePlanReset(scan->plan.nested); + + scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested); + + if (scan->advanceNested || scan->outerJoin) + break; + } + + return true; +} + +/* + * JsonTableFetchRow + * Prepare the next "current" tuple for upcoming GetValue calls. + * Returns FALSE if the row-filter expression returned no more rows. + */ +static bool +JsonTableFetchRow(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableFetchRow"); + + if (cxt->empty) + return false; + + return JsonTableScanNextRow(cxt->root); +} + +/* + * JsonTableGetValue + * Return the value for column number 'colnum' for the current row. + * + * This leaks memory, so be sure to reset often the context in which it's + * called. + */ +static Datum +JsonTableGetValue(TableFuncScanState *state, int colnum, + Oid typid, int32 typmod, bool *isnull) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableGetValue"); + ExprContext *econtext = state->ss.ps.ps_ExprContext; + ExprState *estate = list_nth(state->colvalexprs, colnum); + JsonTableScanState *scan = cxt->colexprscans[colnum]; + Datum result; + + if (scan->currentIsNull) /* NULL from outer/union join */ + { + result = (Datum) 0; + *isnull = true; + } + else if (estate) /* regular column */ + { + Datum saved_caseValue = econtext->caseValue_datum; + bool saved_caseIsNull = econtext->caseValue_isNull; + + /* Pass the value for CaseTestExpr that may be present in colexpr */ + econtext->caseValue_datum = scan->current; + econtext->caseValue_isNull = false; + + result = ExecEvalExpr(estate, econtext, isnull); + + econtext->caseValue_datum = saved_caseValue; + econtext->caseValue_isNull = saved_caseIsNull; + } + else + { + result = Int32GetDatum(scan->ordinal); /* ordinality column */ + *isnull = false; + } + + return result; +} + +/* + * JsonTableDestroyOpaque + */ +static void +JsonTableDestroyOpaque(TableFuncScanState *state) +{ + JsonTableExecContext *cxt = + GetJsonTableExecContext(state, "JsonTableDestroyOpaque"); + + /* not valid anymore */ + cxt->magic = 0; + + state->opaque = NULL; +} + +const TableFuncRoutine JsonbTableRoutine = +{ + JsonTableInitOpaque, + JsonTableSetDocument, + NULL, + NULL, + NULL, + JsonTableFetchRow, + JsonTableGetValue, + JsonTableDestroyOpaque +}; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 62b23964c8..5ea3e2fe4f 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -522,6 +522,8 @@ static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); static void get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit); +static void get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8606,7 +8608,8 @@ get_json_behavior(JsonBehavior *behavior, deparse_context *context, /* * get_json_expr_options * - * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS and + * JSON_TABLE columns. */ static void get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, @@ -9854,6 +9857,9 @@ get_rule_expr(Node *node, deparse_context *context, case JSON_VALUE_OP: appendStringInfoString(buf, "JSON_VALUE("); break; + default: + elog(ERROR, "unexpected JsonExpr type: %d", jexpr->op); + break; } get_rule_expr(jexpr->formatted_expr, context, showimplicit); @@ -11210,16 +11216,14 @@ get_sublink_expr(SubLink *sublink, deparse_context *context) /* ---------- - * get_tablefunc - Parse back a table function + * get_xmltable - Parse back a XMLTABLE function * ---------- */ static void -get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +get_xmltable(TableFunc *tf, deparse_context *context, bool showimplicit) { StringInfo buf = context->buf; - /* XMLTABLE is the only existing implementation. */ - appendStringInfoString(buf, "XMLTABLE("); if (tf->ns_uris != NIL) @@ -11310,6 +11314,271 @@ get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) appendStringInfoChar(buf, ')'); } +/* + * get_json_nested_columns - Parse back nested JSON_TABLE columns + */ +static void +get_json_table_nested_columns(TableFunc *tf, Node *node, + deparse_context *context, bool showimplicit, + bool needcomma) +{ + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_nested_columns(tf, n->larg, context, showimplicit, + needcomma); + get_json_table_nested_columns(tf, n->rarg, context, showimplicit, true); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + if (needcomma) + appendStringInfoChar(context->buf, ','); + + appendStringInfoChar(context->buf, ' '); + appendContextKeyword(context, "NESTED PATH ", 0, 0, 0); + get_const_expr(n->path->value, context, -1); + appendStringInfo(context->buf, " AS %s", quote_identifier(n->path->name)); + get_json_table_columns(tf, n, context, showimplicit); + } +} + +/* + * get_json_table_plan - Parse back a JSON_TABLE plan + */ +static void +get_json_table_plan(TableFunc *tf, Node *node, deparse_context *context, + bool parenthesize) +{ + if (parenthesize) + appendStringInfoChar(context->buf, '('); + + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_plan(tf, n->larg, context, + IsA(n->larg, JsonTableSibling) || + castNode(JsonTableParent, n->larg)->child); + + appendStringInfoString(context->buf, n->cross ? " CROSS " : " UNION "); + + get_json_table_plan(tf, n->rarg, context, + IsA(n->rarg, JsonTableSibling) || + castNode(JsonTableParent, n->rarg)->child); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + appendStringInfoString(context->buf, quote_identifier(n->path->name)); + + if (n->child) + { + appendStringInfoString(context->buf, + n->outerJoin ? " OUTER " : " INNER "); + get_json_table_plan(tf, n->child, context, + IsA(n->child, JsonTableSibling)); + } + } + + if (parenthesize) + appendStringInfoChar(context->buf, ')'); +} + +/* + * get_json_table_columns - Parse back JSON_TABLE columns + */ +static void +get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + ListCell *lc_colname; + ListCell *lc_coltype; + ListCell *lc_coltypmod; + ListCell *lc_colvalexpr; + int colnum = 0; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "COLUMNS (", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forfour(lc_colname, tf->colnames, + lc_coltype, tf->coltypes, + lc_coltypmod, tf->coltypmods, + lc_colvalexpr, tf->colvalexprs) + { + char *colname = strVal(lfirst(lc_colname)); + JsonExpr *colexpr; + Oid typid; + int32 typmod; + bool ordinality; + JsonBehaviorType default_behavior; + + typid = lfirst_oid(lc_coltype); + typmod = lfirst_int(lc_coltypmod); + colexpr = castNode(JsonExpr, lfirst(lc_colvalexpr)); + + if (colnum < node->colMin) + { + colnum++; + continue; + } + + if (colnum > node->colMax) + break; + + if (colnum > node->colMin) + appendStringInfoString(buf, ", "); + + colnum++; + + ordinality = !colexpr; + + appendContextKeyword(context, "", 0, 0, 0); + + appendStringInfo(buf, "%s %s", quote_identifier(colname), + ordinality ? "FOR ORDINALITY" : + format_type_with_typemod(typid, typmod)); + if (ordinality) + continue; + + if (colexpr->op == JSON_EXISTS_OP) + { + appendStringInfoString(buf, " EXISTS"); + default_behavior = JSON_BEHAVIOR_FALSE; + } + else + { + if (colexpr->op == JSON_QUERY_OP) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(typid, &typcategory, &typispreferred); + + if (typcategory == TYPCATEGORY_STRING) + appendStringInfoString(buf, + colexpr->format->format_type == JS_FORMAT_JSONB ? + " FORMAT JSONB" : " FORMAT JSON"); + } + + default_behavior = JSON_BEHAVIOR_NULL; + } + + if (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR) + default_behavior = JSON_BEHAVIOR_ERROR; + + appendStringInfoString(buf, " PATH "); + + get_json_path_spec(colexpr->path_spec, context, showimplicit); + + get_json_expr_options(colexpr, context, default_behavior); + } + + if (node->child) + get_json_table_nested_columns(tf, node->child, context, showimplicit, + node->colMax >= node->colMin); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_json_table - Parse back a JSON_TABLE function + * ---------- + */ +static void +get_json_table(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + + appendStringInfoString(buf, "JSON_TABLE("); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_const_expr(root->path->value, context, -1); + + appendStringInfo(buf, " AS %s", quote_identifier(root->path->name)); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PASSING ", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr((Node *) lfirst(lc2), context, false); + appendStringInfo(buf, " AS %s", + quote_identifier((lfirst_node(String, lc1))->sval) + ); + } + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + } + + get_json_table_columns(tf, root, context, showimplicit); + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PLAN ", 0, 0, 0); + get_json_table_plan(tf, (Node *) root, context, true); + + if (jexpr->on_error->btype != JSON_BEHAVIOR_EMPTY) + get_json_behavior(jexpr->on_error, context, "ERROR"); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_tablefunc - Parse back a table function + * ---------- + */ +static void +get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + /* XMLTABLE and JSON_TABLE are the only existing implementations. */ + + if (tf->functype == TFT_XMLTABLE) + get_xmltable(tf, context, showimplicit); + else if (tf->functype == TFT_JSON_TABLE) + get_json_table(tf, context, showimplicit); +} + /* ---------- * get_from_clause - Parse back a FROM clause * diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 584bf7001a..91453681e3 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1879,6 +1879,8 @@ typedef struct TableFuncScanState ExprState *rowexpr; /* state for row-generating expression */ List *colexprs; /* state for column-generating expression */ List *coldefexprs; /* state for column default expressions */ + List *colvalexprs; /* state for column value expression */ + List *passingvalexprs; /* state for PASSING argument expression */ List *ns_names; /* same as TableFunc.ns_names */ List *ns_uris; /* list of states of namespace URI exprs */ Bitmapset *notnulls; /* nullability flag for each output column */ diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index c1d437b44e..076b1ddab3 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -113,6 +113,8 @@ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, extern JsonValueExpr *makeJsonValueExpr(Expr *raw_expr, Expr *formatted_expr, JsonFormat *format); extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); +extern Node *makeJsonTableJoinedPlan(JsonTablePlanJoinType type, + Node *plan1, Node *plan2, int location); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4e515548d2..5f487d1f2d 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1733,6 +1733,19 @@ typedef enum JsonQuotes */ typedef char *JsonPathSpec; +/* + * JsonTableColumnType - + * enumeration of JSON_TABLE column types + */ +typedef enum JsonTableColumnType +{ + JTC_FOR_ORDINALITY, + JTC_REGULAR, + JTC_EXISTS, + JTC_FORMATTED, + JTC_NESTED, +} JsonTableColumnType; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1786,6 +1799,83 @@ typedef struct JsonFuncExpr int location; /* token location, or -1 if unknown */ } JsonFuncExpr; +/* + * JsonTableColumn - + * untransformed representation of JSON_TABLE column + */ +typedef struct JsonTableColumn +{ + NodeTag type; + JsonTableColumnType coltype; /* column type */ + char *name; /* column name */ + TypeName *typeName; /* column type name */ + char *pathspec; /* path specification, if any */ + char *pathname; /* path name, if any */ + JsonFormat *format; /* JSON format clause, if specified */ + JsonWrapper wrapper; /* WRAPPER behavior for formatted columns */ + JsonQuotes quotes; /* omit or keep quotes on scalar strings? */ + List *columns; /* nested columns */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + int location; /* token location, or -1 if unknown */ +} JsonTableColumn; + +/* + * JsonTablePlanType - + * flags for JSON_TABLE plan node types representation + */ +typedef enum JsonTablePlanType +{ + JSTP_DEFAULT, + JSTP_SIMPLE, + JSTP_JOINED, +} JsonTablePlanType; + +/* + * JsonTablePlanJoinType - + * flags for JSON_TABLE join types representation + */ +typedef enum JsonTablePlanJoinType +{ + JSTPJ_INNER = 0x01, + JSTPJ_OUTER = 0x02, + JSTPJ_CROSS = 0x04, + JSTPJ_UNION = 0x08, +} JsonTablePlanJoinType; + +typedef struct JsonTablePlan JsonTablePlan; + +/* + * JsonTablePlan - + * untransformed representation of JSON_TABLE plan node + */ +struct JsonTablePlan +{ + NodeTag type; + JsonTablePlanType plan_type; /* plan type */ + JsonTablePlanJoinType join_type; /* join type (for joined plan only) */ + JsonTablePlan *plan1; /* first joined plan */ + JsonTablePlan *plan2; /* second joined plan */ + char *pathname; /* path name (for simple plan only) */ + int location; /* token location, or -1 if unknown */ +}; + +/* + * JsonTable - + * untransformed representation of JSON_TABLE + */ +typedef struct JsonTable +{ + NodeTag type; + JsonCommon *common; /* common JSON path syntax fields */ + List *columns; /* list of JsonTableColumn */ + JsonTablePlan *plan; /* join plan, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + Alias *alias; /* table alias in FROM clause */ + bool lateral; /* does it have LATERAL prefix? */ + int location; /* token location, or -1 if unknown */ +} JsonTable; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index a6283ba2c0..aecd09feb7 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -86,8 +86,14 @@ typedef struct RangeVar int location; } RangeVar; +typedef enum TableFuncType +{ + TFT_XMLTABLE, + TFT_JSON_TABLE +} TableFuncType; + /* - * TableFunc - node for a table function, such as XMLTABLE. + * TableFunc - node for a table function, such as XMLTABLE or JSON_TABLE. * * Entries in the ns_names list are either String nodes containing * literal namespace names, or NULL pointers to represent DEFAULT. @@ -95,6 +101,8 @@ typedef struct RangeVar typedef struct TableFunc { NodeTag type; + /* XMLTABLE or JSON_TABLE */ + TableFuncType functype; /* list of namespace URI expressions */ List *ns_uris pg_node_attr(query_jumble_ignore); /* list of namespace names or NULL */ @@ -115,8 +123,14 @@ typedef struct TableFunc List *colexprs; /* list of column default expressions */ List *coldefexprs pg_node_attr(query_jumble_ignore); + /* list of column value expressions */ + List *colvalexprs pg_node_attr(query_jumble_ignore); + /* list of PASSING argument expressions */ + List *passingvalexprs pg_node_attr(query_jumble_ignore); /* nullability flag for each output column */ Bitmapset *notnulls pg_node_attr(query_jumble_ignore); + /* JSON_TABLE plan */ + Node *plan pg_node_attr(query_jumble_ignore); /* counts from 0; -1 if none specified */ int ordinalitycol pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ @@ -1552,7 +1566,8 @@ typedef enum JsonExprOp { JSON_VALUE_OP, /* JSON_VALUE() */ JSON_QUERY_OP, /* JSON_QUERY() */ - JSON_EXISTS_OP /* JSON_EXISTS() */ + JSON_EXISTS_OP, /* JSON_EXISTS() */ + JSON_TABLE_OP /* JSON_TABLE() */ } JsonExprOp; /* @@ -1771,6 +1786,48 @@ typedef struct JsonExpr int location; /* token location, or -1 if unknown */ } JsonExpr; +/* + * JsonTablePath + * A JSON path expression to be computed as part of evaluating + * a JSON_TABLE plan node + */ +typedef struct JsonTablePath +{ + NodeTag type; + + Const *value; + char *name; +} JsonTablePath; + +/* + * JsonTableParent - + * transformed representation of parent JSON_TABLE plan node + */ +typedef struct JsonTableParent +{ + NodeTag type; + JsonTablePath *path; + Node *child; /* nested columns, if any */ + bool outerJoin; /* outer or inner join for nested columns? */ + int colMin; /* min column index in the resulting column + * list */ + int colMax; /* max column index in the resulting column + * list */ + bool errorOnError; /* ERROR/EMPTY ON ERROR behavior */ +} JsonTableParent; + +/* + * JsonTableSibling - + * transformed representation of joined sibling JSON_TABLE plan node + */ +typedef struct JsonTableSibling +{ + NodeTag type; + Node *larg; /* left join node */ + Node *rarg; /* right join node */ + bool cross; /* cross or union join? */ +} JsonTableSibling; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0954d9fc7b..f88a6c9ac6 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -242,6 +242,7 @@ PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_table", JSON_TABLE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) @@ -284,6 +285,7 @@ PG_KEYWORD("names", NAMES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("national", NATIONAL, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("natural", NATURAL, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("nchar", NCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("nested", NESTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("new", NEW, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("next", NEXT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("nfc", NFC, UNRESERVED_KEYWORD, BARE_LABEL) @@ -334,7 +336,9 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h index 8e26482db4..7152a8ef48 100644 --- a/src/include/parser/parse_clause.h +++ b/src/include/parser/parse_clause.h @@ -51,4 +51,7 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle, extern Index assignSortGroupRef(TargetEntry *tle, List *tlist); extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList); +/* functions in parse_jsontable.c */ +extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt); + #endif /* PARSE_CLAUSE_H */ diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index 5a37133847..7eb0ccf4e4 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -15,6 +15,7 @@ #define JSONPATH_H #include "fmgr.h" +#include "executor/tablefunc.h" #include "nodes/pg_list.h" #include "nodes/primnodes.h" #include "utils/jsonb.h" @@ -288,4 +289,6 @@ extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars); +extern PGDLLIMPORT const TableFuncRoutine JsonbTableRoutine; + #endif diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out index 8b87580752..995f267404 100644 --- a/src/test/regress/expected/json_sqljson.out +++ b/src/test/regress/expected/json_sqljson.out @@ -16,3 +16,9 @@ ERROR: JSON_QUERY() is not yet implemented for the json type LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); ^ HINT: Try casting the argument to jsonb +-- JSON_TABLE +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); +ERROR: JSON_TABLE() is not yet implemented for the json type +LINE 1: SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo ... + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out index 26ba66ee20..5dda19dd45 100644 --- a/src/test/regress/expected/jsonb_sqljson.out +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -1040,3 +1040,1189 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.dateti ERROR: functions in index expression must be marked IMMUTABLE CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; +-- JSON_TABLE +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); +ERROR: syntax error at or near "(" +LINE 1: SELECT JSON_TABLE('[]', '$'); + ^ +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); +ERROR: syntax error at or near ")" +LINE 1: SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + ^ +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); +ERROR: JSON_TABLE function has 1 columns available but 2 columns specified +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + foo +----- +(0 rows) + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + item | foo +------+----- + 123 | +(1 row) + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); +CREATE TEMP TABLE json_table_test (js) AS + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ); +-- Regular "unformatted" columns +SELECT * +FROM json_table_test vals + LEFT OUTER JOIN + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$' + ) + ) jt + ON true; + js | id | int | text | char(4) | bool | numeric | domain | js | jb +---------------------------------------------------------------------------------------+----+-----+---------+---------+------+---------+---------+--------------+-------------- + 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 + [] | | | | | | | | | + {} | 1 | | | | | | | {} | {} + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 2 | 1 | 1.23 | 1.23 | | 1.23 | 1.23 | 1.23 | 1.23 + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 3 | 2 | 2 | 2 | | 2 | 2 | "2" | "2" + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 4 | | aaaaaaa | aaaa | | | aaaaaaa | "aaaaaaa" | "aaaaaaa" + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 5 | | foo | foo | | | | "foo" | "foo" + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 6 | | | | | | | null | null + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 7 | 0 | false | fals | f | | false | false | false + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 8 | 1 | true | true | t | | true | true | true + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 9 | | | | | | | {"aaa": 123} | {"aaa": 123} + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 10 | | [1,2] | [1,2 | | | [1,2] | "[1,2]" | "[1,2]" + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 11 | | "str" | "str | | | "str" | "\"str\"" | "\"str\"" +(14 rows) + +-- "formatted" columns +SELECT * +FROM json_table_test vals + LEFT OUTER JOIN + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES + ) + ) jt + ON true; + js | id | jst | jsc | jsv | jsb | jsbq +---------------------------------------------------------------------------------------+----+--------------+------+------+--------------+-------------- + 1 | 1 | 1 | 1 | 1 | 1 | 1 + [] | | | | | | + {} | 1 | {} | {} | {} | {} | {} + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 1 | 1 | 1 | 1 | 1 | 1 + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 2 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 3 | "2" | "2" | "2" | "2" | 2 + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 4 | "aaaaaaa" | "aaa | "aaa | "aaaaaaa" | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 5 | "foo" | "foo | "foo | "foo" | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 6 | null | null | null | null | null + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 7 | false | fals | fals | false | false + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 8 | true | true | true | true | true + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 9 | {"aaa": 123} | {"aa | {"aa | {"aaa": 123} | {"aaa": 123} + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 10 | "[1,2]" | "[1, | "[1, | "[1,2]" | [1, 2] + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 11 | "\"str\"" | "\"s | "\"s | "\"str\"" | "str" +(14 rows) + +-- EXISTS columns +SELECT * +FROM json_table_test vals + LEFT OUTER JOIN + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR + ) + ) jt + ON true; + js | id | exists1 | exists2 | exists3 | exists4 +---------------------------------------------------------------------------------------+----+---------+---------+---------+--------- + 1 | 1 | f | 0 | | false + [] | | | | | + {} | 1 | f | 0 | | false + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 1 | f | 0 | | false + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 2 | f | 0 | | false + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 3 | f | 0 | | false + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 4 | f | 0 | | false + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 5 | f | 0 | | false + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 6 | f | 0 | | false + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 7 | f | 0 | | false + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 8 | f | 0 | | false + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 9 | t | 1 | 1 | true + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 10 | f | 0 | | false + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 11 | f | 0 | | false +(14 rows) + +-- Other miscellanous checks +SELECT * +FROM json_table_test vals + LEFT OUTER JOIN + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + aaa int, -- "aaa" has implicit path '$."aaa"' + aaa1 int PATH '$.aaa', + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + js | id | aaa | aaa1 | js2 | jsb2w | jsb2q | ia | ta | jba +---------------------------------------------------------------------------------------+----+-----+------+--------------+----------------+--------------+----+----+----- + 1 | 1 | | | 1 | [1] | 1 | | | + [] | | | | | | | | | + {} | 1 | | | {} | [{}] | {} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 1 | | | 1 | [1] | 1 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 2 | | | 1.23 | [1.23] | 1.23 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 3 | | | "2" | ["2"] | 2 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 4 | | | "aaaaaaa" | ["aaaaaaa"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 5 | | | "foo" | ["foo"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 6 | | | null | [null] | null | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 7 | | | false | [false] | false | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 8 | | | true | [true] | true | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 9 | 123 | 123 | {"aaa": 123} | [{"aaa": 123}] | {"aaa": 123} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 10 | | | "[1,2]" | ["[1,2]"] | [1, 2] | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 11 | | | "\"str\"" | ["\"str\""] | "str" | | | +(14 rows) + +-- JSON_TABLE: Test backward parsing +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); +\sv jsonb_table_view +CREATE OR REPLACE VIEW public.jsonb_table_view AS + SELECT id, + "int", + text, + "char(4)", + bool, + "numeric", + domain, + js, + jb, + jst, + jsc, + jsv, + jsb, + jsbq, + aaa, + aaa1, + exists1, + exists2, + exists3, + js2, + jsb2w, + jsb2q, + ia, + ta, + jba, + a1, + b1, + a11, + a21, + a22 + FROM JSON_TABLE( + 'null'::jsonb, '$[*]' AS json_table_path_1 + PASSING + 1 + 2 AS a, + '"foo"'::json AS "b c" + COLUMNS ( + id FOR ORDINALITY, + "int" integer PATH '$', + text text PATH '$', + "char(4)" character(4) PATH '$', + bool boolean PATH '$', + "numeric" numeric PATH '$', + domain jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc character(4) FORMAT JSON PATH '$', + jsv character varying(4) FORMAT JSON PATH '$', + jsb jsonb PATH '$', + jsbq jsonb PATH '$' OMIT QUOTES, + aaa integer PATH '$."aaa"', + aaa1 integer PATH '$."aaa"', + exists1 boolean EXISTS PATH '$."aaa"', + exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia integer[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' AS p1 + COLUMNS ( + a1 integer PATH '$."a1"', + b1 text PATH '$."b1"', + NESTED PATH '$[*]' AS "p1 1" + COLUMNS ( + a11 text PATH '$."a11"' + ) + ), + NESTED PATH '$[2]' AS p2 + COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" + COLUMNS ( + a21 text PATH '$."a21"' + ), + NESTED PATH '$[*]' AS p22 + COLUMNS ( + a22 text PATH '$."a22"' + ) + ) + ) + PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22)))) + ) +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Table Function Scan on "json_table" + Output: "json_table".id, "json_table"."int", "json_table".text, "json_table"."char(4)", "json_table".bool, "json_table"."numeric", "json_table".domain, "json_table".js, "json_table".jb, "json_table".jst, "json_table".jsc, "json_table".jsv, "json_table".jsb, "json_table".jsbq, "json_table".aaa, "json_table".aaa1, "json_table".exists1, "json_table".exists2, "json_table".exists3, "json_table".js2, "json_table".jsb2w, "json_table".jsb2q, "json_table".ia, "json_table".ta, "json_table".jba, "json_table".a1, "json_table".b1, "json_table".a11, "json_table".a21, "json_table".a22 + Table Function Call: JSON_TABLE('null'::jsonb, '$[*]' AS json_table_path_1 PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (id FOR ORDINALITY, "int" integer PATH '$', text text PATH '$', "char(4)" character(4) PATH '$', bool boolean PATH '$', "numeric" numeric PATH '$', domain jsonb_test_domain PATH '$', js json PATH '$', jb jsonb PATH '$', jst text FORMAT JSON PATH '$', jsc character(4) FORMAT JSON PATH '$', jsv character varying(4) FORMAT JSON PATH '$', jsb jsonb PATH '$', jsbq jsonb PATH '$' OMIT QUOTES, aaa integer PATH '$."aaa"', aaa1 integer PATH '$."aaa"', exists1 boolean EXISTS PATH '$."aaa"', exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, js2 json PATH '$', jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER, jsb2q jsonb PATH '$' OMIT QUOTES, ia integer[] PATH '$', ta text[] PATH '$', jba jsonb[] PATH '$', NESTED PATH '$[1]' AS p1 COLUMNS (a1 integer PATH '$."a1"', b1 text PATH '$."b1"', NESTED PATH '$[*]' AS "p1 1" COLUMNS (a11 text PATH '$."a11"')), NESTED PATH '$[2]' AS p2 COLUMNS ( NESTED PATH '$[*]' AS "p2:1" COLUMNS (a21 text PATH '$."a21"'), NESTED PATH '$[*]' AS p22 COLUMNS (a22 text PATH '$."a22"'))) PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22))))) +(3 rows) + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; +-- JSON_TABLE: only one FOR ORDINALITY columns allowed +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (id FOR ORDINALITY, id2 FOR ORDINALITY, a int PATH '$.a' ERROR ON EMPTY)) jt; +ERROR: cannot use more than one FOR ORDINALITY column +LINE 1: ..._TABLE(jsonb '1', '$' COLUMNS (id FOR ORDINALITY, id2 FOR OR... + ^ +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (id FOR ORDINALITY, a int PATH '$' ERROR ON EMPTY)) jt; + id | a +----+--- + 1 | 1 +(1 row) + +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + js | a +-------+--- + 1 | 1 + "err" | +(2 rows) + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; + a +--- + +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: jsonpath member accessor can only be applied to an object +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: no SQL/JSON item +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 1 +(1 row) + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); + a +--- + 0 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to smallint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to bigint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to real +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 E... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); + a +----- + fal +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to json +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to jsonb +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EX... + ^ +-- JSON_TABLE: WRAPPER/QUOTES clauses on scalar columns don't work without an +-- explicit FORMAT clause +SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text PATH '$' KEEP QUOTES ON SCALAR STRING)); +ERROR: cannot use OMIT/KEEP QUOTES clause for scalar columns without also specifying FORMAT clause +LINE 1: ...T * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text ... + ^ +SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text FORMAT JSON PATH '$' KEEP QUOTES ON SCALAR STRING)); + item +--------- + "world" +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text PATH '$' WITH WRAPPER)); +ERROR: cannot use WITH WRAPPER clause for scalar columns without also specifying FORMAT clause +LINE 1: ...T * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text ... + ^ +SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text FORMAT JSON PATH '$' WITH WRAPPER)); + item +----------- + ["world"] +(1 row) + +-- JSON_TABLE: nested paths and plans +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb '[]', '$' -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 4: NESTED PATH '$' COLUMNS ( -- AS <path name> required here + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: b +HINT: JSON_TABLE column names must be distinct from one another. +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another. +-- JSON_TABLE: plan validation +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p1) + ^ +DETAIL: Path name mismatch: expected p0 but p1 is given. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 4: NESTED PATH '$' AS p1 COLUMNS ( + ^ +DETAIL: Plan node for nested path p1 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 UNION p1 UNION p11) + ^ +DETAIL: Expected INNER or OUTER JSON_TABLE plan node. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 8: NESTED PATH '$' AS p2 COLUMNS ( + ^ +DETAIL: Plan node for nested path p2 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 5: NESTED PATH '$' AS p11 COLUMNS ( foo int ), + ^ +DETAIL: Plan node for nested path p11 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 12: PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) + ^ +DETAIL: Plan node contains some extra or duplicate sibling nodes. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 6: NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ^ +DETAIL: Plan node for nested path p12 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; +ERROR: invalid JSON_TABLE plan +LINE 9: NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ^ +DETAIL: Plan node for nested path p21 was not found in plan. +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + bar | foo | baz +-----+-----+----- +(0 rows) + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; +ERROR: invalid JSON_TABLE expression +LINE 2: jsonb 'null', 'strict $[*]' -- without root path name + ^ +DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used +-- JSON_TABLE: plan execution +CREATE TEMP TABLE jsonb_table_test (js jsonb); +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(11 rows) + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + n | a | c | b +---+----+----+--- + 1 | 1 | | + 2 | 2 | 10 | + 2 | 2 | | + 2 | 2 | 20 | + 2 | 2 | | 1 + 2 | 2 | | 2 + 2 | 2 | | 3 + 3 | 3 | | 1 + 3 | 3 | | 2 + 4 | -1 | | 1 + 4 | -1 | | 2 +(11 rows) + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + n | a | b | c +---+----+---+---- + 2 | 2 | 1 | + 2 | 2 | 2 | + 2 | 2 | 3 | + 2 | 2 | | 10 + 2 | 2 | | + 2 | 2 | | 20 + 3 | 3 | 1 | + 3 | 3 | 2 | + 4 | -1 | 1 | + 4 | -1 | 2 | +(10 rows) + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + n | a | b | c +---+---+---+---- + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 +(9 rows) + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + n | a | b | c +---+----+---+---- + 1 | 1 | | + 2 | 2 | 1 | 10 + 2 | 2 | 1 | + 2 | 2 | 1 | 20 + 2 | 2 | 2 | 10 + 2 | 2 | 2 | + 2 | 2 | 2 | 20 + 2 | 2 | 3 | 10 + 2 | 2 | 3 | + 2 | 2 | 3 | 20 + 3 | 3 | | + 4 | -1 | | +(12 rows) + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + n | a | b | b1 | c | c1 | b +---+---+--------------+-----+------+----+----- + 1 | 1 | [1, 10] | 1 | 1 | | 101 + 1 | 1 | [1, 10] | 1 | null | | 101 + 1 | 1 | [1, 10] | 1 | 2 | | 101 + 1 | 1 | [1, 10] | 10 | 1 | | 110 + 1 | 1 | [1, 10] | 10 | null | | 110 + 1 | 1 | [1, 10] | 10 | 2 | | 110 + 1 | 1 | [2] | 2 | 1 | | 102 + 1 | 1 | [2] | 2 | null | | 102 + 1 | 1 | [2] | 2 | 2 | | 102 + 1 | 1 | [3, 30, 300] | 3 | 1 | | 103 + 1 | 1 | [3, 30, 300] | 3 | null | | 103 + 1 | 1 | [3, 30, 300] | 3 | 2 | | 103 + 1 | 1 | [3, 30, 300] | 30 | 1 | | 130 + 1 | 1 | [3, 30, 300] | 30 | null | | 130 + 1 | 1 | [3, 30, 300] | 30 | 2 | | 130 + 1 | 1 | [3, 30, 300] | 300 | 1 | | 400 + 1 | 1 | [3, 30, 300] | 300 | null | | 400 + 1 | 1 | [3, 30, 300] | 300 | 2 | | 400 + 2 | 2 | | | | | + 3 | | | | | | +(20 rows) + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + x | y | y | z +---+---+--------------+--- + 2 | 1 | [1, 2, 3] | 1 + 2 | 1 | [1, 2, 3] | 2 + 2 | 1 | [1, 2, 3] | 3 + 3 | 1 | [1, 2, 3] | 1 + 3 | 1 | [1, 2, 3] | 2 + 3 | 1 | [1, 2, 3] | 3 + 3 | 1 | [2, 3, 4, 5] | 2 + 3 | 1 | [2, 3, 4, 5] | 3 + 3 | 1 | [2, 3, 4, 5] | 4 + 3 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [1, 2, 3] | 1 + 4 | 1 | [1, 2, 3] | 2 + 4 | 1 | [1, 2, 3] | 3 + 4 | 1 | [2, 3, 4, 5] | 2 + 4 | 1 | [2, 3, 4, 5] | 3 + 4 | 1 | [2, 3, 4, 5] | 4 + 4 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [3, 4, 5, 6] | 3 + 4 | 1 | [3, 4, 5, 6] | 4 + 4 | 1 | [3, 4, 5, 6] | 5 + 4 | 1 | [3, 4, 5, 6] | 6 + 2 | 2 | [1, 2, 3] | 2 + 2 | 2 | [1, 2, 3] | 3 + 3 | 2 | [1, 2, 3] | 2 + 3 | 2 | [1, 2, 3] | 3 + 3 | 2 | [2, 3, 4, 5] | 2 + 3 | 2 | [2, 3, 4, 5] | 3 + 3 | 2 | [2, 3, 4, 5] | 4 + 3 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [1, 2, 3] | 2 + 4 | 2 | [1, 2, 3] | 3 + 4 | 2 | [2, 3, 4, 5] | 2 + 4 | 2 | [2, 3, 4, 5] | 3 + 4 | 2 | [2, 3, 4, 5] | 4 + 4 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [3, 4, 5, 6] | 3 + 4 | 2 | [3, 4, 5, 6] | 4 + 4 | 2 | [3, 4, 5, 6] | 5 + 4 | 2 | [3, 4, 5, 6] | 6 + 2 | 3 | [1, 2, 3] | 3 + 3 | 3 | [1, 2, 3] | 3 + 3 | 3 | [2, 3, 4, 5] | 3 + 3 | 3 | [2, 3, 4, 5] | 4 + 3 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [1, 2, 3] | 3 + 4 | 3 | [2, 3, 4, 5] | 3 + 4 | 3 | [2, 3, 4, 5] | 4 + 4 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [3, 4, 5, 6] | 3 + 4 | 3 | [3, 4, 5, 6] | 4 + 4 | 3 | [3, 4, 5, 6] | 5 + 4 | 3 | [3, 4, 5, 6] | 6 +(52 rows) + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; +ERROR: could not find jsonpath variable "x" +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); + json_value +------------ + foo +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_query +------------ + 123 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); + json_query +------------ + [123] +(1 row) + +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +ERROR: syntax error at or near " " of jsonpath input +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); +ERROR: only string constants supported in JSON_TABLE path specification +LINE 1: SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || '... + ^ diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql index 4f30fa46b9..df4a430d88 100644 --- a/src/test/regress/sql/json_sqljson.sql +++ b/src/test/regress/sql/json_sqljson.sql @@ -9,3 +9,7 @@ SELECT JSON_VALUE(NULL FORMAT JSON, '$'); -- JSON_QUERY SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + +-- JSON_TABLE + +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql index a973de1e57..e2a43acd76 100644 --- a/src/test/regress/sql/jsonb_sqljson.sql +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -325,3 +325,677 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; + +-- JSON_TABLE + +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); + +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + +SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2); + +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); +CREATE TEMP TABLE json_table_test (js) AS + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ); + +-- Regular "unformatted" columns +SELECT * +FROM json_table_test vals + LEFT OUTER JOIN + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$' + ) + ) jt + ON true; + +-- "formatted" columns +SELECT * +FROM json_table_test vals + LEFT OUTER JOIN + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES + ) + ) jt + ON true; + +-- EXISTS columns +SELECT * +FROM json_table_test vals + LEFT OUTER JOIN + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR + ) + ) jt + ON true; + +-- Other miscellanous checks +SELECT * +FROM json_table_test vals + LEFT OUTER JOIN + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + aaa int, -- "aaa" has implicit path '$."aaa"' + aaa1 int PATH '$.aaa', + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + +-- JSON_TABLE: Test backward parsing + +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + + NESTED PATH '$[1]' AS p1 COLUMNS ( + a1 int, + NESTED PATH '$[*]' AS "p1 1" COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' AS p2 COLUMNS ( + NESTED PATH '$[*]' AS "p2:1" COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' AS p22 COLUMNS ( + a22 text + ) + ) + ) + ); + +\sv jsonb_table_view + +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; + +-- JSON_TABLE: only one FOR ORDINALITY columns allowed +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (id FOR ORDINALITY, id2 FOR ORDINALITY, a int PATH '$.a' ERROR ON EMPTY)) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (id FOR ORDINALITY, a int PATH '$' ERROR ON EMPTY)) jt; + +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); + +-- JSON_TABLE: WRAPPER/QUOTES clauses on scalar columns don't work without an +-- explicit FORMAT clause +SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text PATH '$' KEEP QUOTES ON SCALAR STRING)); +SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text FORMAT JSON PATH '$' KEEP QUOTES ON SCALAR STRING)); +SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text PATH '$' WITH WRAPPER)); +SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text FORMAT JSON PATH '$' WITH WRAPPER)); + +-- JSON_TABLE: nested paths and plans + +-- Should fail (JSON_TABLE columns must contain explicit AS path +-- specifications if explicit PLAN clause is used) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' -- AS <path name> required here + COLUMNS ( + foo int PATH '$' + ) + PLAN DEFAULT (UNION) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS path1 + COLUMNS ( + NESTED PATH '$' COLUMNS ( -- AS <path name> required here + foo int PATH '$' + ) + ) + PLAN DEFAULT (UNION) +) jt; + +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + a int + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' AS a + COLUMNS ( + b int, + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' AS b + COLUMNS ( + c int + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' AS a + COLUMNS ( + c int + ) + ) + ) +) jt; + +-- JSON_TABLE: plan validation + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p1) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER p3) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 UNION p1 UNION p11) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p13)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER (p1 CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 UNION p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER p11) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', '$[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2)) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' AS p0 + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb 'null', 'strict $[*]' -- without root path name + COLUMNS ( + NESTED PATH '$' AS p1 COLUMNS ( + NESTED PATH '$' AS p11 COLUMNS ( foo int ), + NESTED PATH '$' AS p12 COLUMNS ( bar int ) + ), + NESTED PATH '$' AS p2 COLUMNS ( + NESTED PATH '$' AS p21 COLUMNS ( baz int ) + ) + ) + PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)) +) jt; + +-- JSON_TABLE: plan execution + +CREATE TEMP TABLE jsonb_table_test (js jsonb); + +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); + +-- unspecified plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + ) jt; + +-- default plan (outer, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, union) + ) jt; + +-- specific plan (p outer (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb union pc)) + ) jt; + +-- specific plan (p outer (pc union pb)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pc union pb)) + ) jt; + +-- default plan (inner, union) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (inner) + ) jt; + +-- specific plan (p inner (pb union pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb union pc)) + ) jt; + +-- default plan (inner, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (cross, inner) + ) jt; + +-- specific plan (p inner (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p inner (pb cross pc)) + ) jt; + +-- default plan (outer, cross) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan default (outer, cross) + ) jt; + +-- specific plan (p outer (pb cross pc)) +select + jt.* +from + jsonb_table_test jtt, + json_table ( + jtt.js,'strict $[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on empty, + nested path 'strict $.b[*]' as pb columns ( b int path '$' ), + nested path 'strict $.c[*]' as pc columns ( c int path '$' ) + ) + plan (p outer (pb cross pc)) + ) jt; + + +select + jt.*, b1 + 100 as b +from + json_table (jsonb + '[ + {"a": 1, "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]}, + {"a": 2, "b": [10, 20], "c": [1, null, 2]}, + {"x": "3", "b": [11, 22, 33, 44]} + ]', + '$[*]' as p + columns ( + n for ordinality, + a int path 'lax $.a' default -1 on error, + nested path 'strict $.b[*]' as pb columns ( + b text format json path '$', + nested path 'strict $[*]' as pb1 columns ( + b1 int path '$' + ) + ), + nested path 'strict $.c[*]' as pc columns ( + c text format json path '$', + nested path 'strict $[*]' as pc1 columns ( + c1 int path '$' + ) + ) + ) + --plan default(outer, cross) + plan(p outer ((pb inner pb1) cross (pc outer pc1))) + ) jt; + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; + +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 53595ce180..62026c4279 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1310,6 +1310,7 @@ JsonPathMutableContext JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback +JsonPathSpec JsonPathString JsonPathVarCallback JsonPathVariable @@ -1319,6 +1320,17 @@ JsonReturning JsonScalarExpr JsonSemAction JsonSerializeExpr +JsonTable +JsonTableColumn +JsonTableColumnType +JsonTableContext +JsonTableJoinState +JsonTableParent +JsonTablePlan +JsonTablePlanJoinType +JsonTablePlanType +JsonTableScanState +JsonTableSibling JsonTokenType JsonTransformStringValuesAction JsonTypeCategory @@ -2774,6 +2786,7 @@ TableFunc TableFuncRoutine TableFuncScan TableFuncScanState +TableFuncType TableInfo TableLikeClause TableSampleClause -- 2.35.3
v11-0001-SQL-JSON-query-functions.patch (application/octet-stream, 210.0K) download From d96c18a51abeb17430ee86c796af7b30d300dd88 Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Tue, 18 Jul 2023 17:58:35 +0900 Subject: [PATCH v11 1/3] SQL/JSON query functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This introduces the SQL/JSON functions for querying JSON data using jsonpath expressions. The functions are: JSON_EXISTS() JSON_QUERY() JSON_VALUE() JSON_EXISTS() tests if the jsonpath expression applied to the jsonb value yields any values. JSON_VALUE() must return a single value, and an error occurs if it tries to return multiple values. JSON_QUERY() must return a json object or array, and there are various WRAPPER options for handling scalar or multi-value results. Both these functions have options for handling EMPTY and ERROR conditions. All of these functions only operate on jsonb. The workaround for now is to cast the argument to jsonb. Author: Nikita Glukhov <n.gluhov@postgrespro.ru> Author: Teodor Sigaev <teodor@sigaev.ru> Author: Oleg Bartunov <obartunov@gmail.com> Author: Alexander Korotkov <aekorotkov@gmail.com> Author: Andrew Dunstan <andrew@dunslane.net> Author: Amit Langote <amitlangote09@gmail.com> Reviewers have included (in no particular order) Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera, jian he Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- doc/src/sgml/func.sgml | 148 +++ src/backend/executor/execExpr.c | 429 ++++++++ src/backend/executor/execExprInterp.c | 509 ++++++++- src/backend/jit/llvm/llvmjit_expr.c | 250 +++++ src/backend/jit/llvm/llvmjit_types.c | 5 + src/backend/nodes/makefuncs.c | 15 + src/backend/nodes/nodeFuncs.c | 183 ++++ src/backend/optimizer/path/costsize.c | 3 +- src/backend/optimizer/util/clauses.c | 19 + src/backend/parser/gram.y | 348 ++++++- src/backend/parser/parse_collate.c | 7 + src/backend/parser/parse_expr.c | 517 ++++++++- src/backend/parser/parse_target.c | 15 + src/backend/utils/adt/formatting.c | 44 + src/backend/utils/adt/jsonb.c | 62 ++ src/backend/utils/adt/jsonfuncs.c | 170 ++- src/backend/utils/adt/jsonpath.c | 255 +++++ src/backend/utils/adt/jsonpath_exec.c | 391 ++++++- src/backend/utils/adt/ruleutils.c | 137 +++ src/include/executor/execExpr.h | 166 +++ src/include/executor/executor.h | 1 + src/include/nodes/execnodes.h | 3 + src/include/nodes/makefuncs.h | 1 + src/include/nodes/parsenodes.h | 59 ++ src/include/nodes/primnodes.h | 109 ++ src/include/parser/kwlist.h | 11 + src/include/utils/formatting.h | 2 +- src/include/utils/jsonb.h | 3 + src/include/utils/jsonfuncs.h | 5 + src/include/utils/jsonpath.h | 27 + src/interfaces/ecpg/preproc/ecpg.trailer | 28 + src/test/regress/expected/json_sqljson.out | 18 + src/test/regress/expected/jsonb_sqljson.out | 1042 +++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/json_sqljson.sql | 11 + src/test/regress/sql/jsonb_sqljson.sql | 327 ++++++ src/tools/pgindent/typedefs.list | 21 + 37 files changed, 5252 insertions(+), 91 deletions(-) create mode 100644 src/test/regress/expected/json_sqljson.out create mode 100644 src/test/regress/expected/jsonb_sqljson.out create mode 100644 src/test/regress/sql/json_sqljson.sql create mode 100644 src/test/regress/sql/jsonb_sqljson.sql diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index be2f54c914..b26fdd84b0 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16991,6 +16991,154 @@ array w/o UK? | t </tbody> </tgroup> </table> + + <para> + <xref linkend="functions-sqljson-querying"/> details the SQL/JSON + functions that can be used to query JSON data. + </para> + + <note> + <para> + SQL/JSON paths can only be applied to the <type>jsonb</type> type, so it + might be necessary to cast the <replaceable>context_item</replaceable> + argument of these functions to <type>jsonb</type>. + </para> + </note> + + <table id="functions-sqljson-querying"> + <title>SQL/JSON Query Functions</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="func_table_entry"><para role="func_signature"> + Function signature + </para> + <para> + Description + </para> + <para> + Example(s) + </para></entry> + </row> + </thead> + <tbody> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_exists</primary></indexterm> + <function>json_exists</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>TRUE</literal> | <literal>FALSE</literal> |<literal> UNKNOWN</literal> | <literal>ERROR</literal> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns true if the SQL/JSON <replaceable>path_expression</replaceable> + applied to the <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s yields any items. + The <literal>ON ERROR</literal> clause specifies what is returned if + an error occurs; the default is to return <literal>FALSE</literal>. + Note that if the <replaceable>path_expression</replaceable> + is <literal>strict</literal>, an error is generated if it yields no items. + </para> + <para> + <literal>json_exists(jsonb '{"key1": [1,2,3]}', 'strict $.key1[*] ? (@ > 2)')</literal> + <returnvalue>t</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'lax $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>f</returnvalue> + </para> + <para> + <literal>json_exists(jsonb '{"a": [1,2,3]}', 'strict $.a[5]' ERROR ON ERROR)</literal> + <returnvalue>ERROR: jsonpath array subscript is out of bounds</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_query</primary></indexterm> + <function>json_query</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional> + <optional> { <literal>WITHOUT</literal> | <literal>WITH</literal> { <literal>CONDITIONAL</literal> | <optional><literal>UNCONDITIONAL</literal></optional> } } <optional> <literal>ARRAY</literal> </optional> <literal>WRAPPER</literal> </optional> + <optional> { <literal>KEEP</literal> | <literal>OMIT</literal> } <literal>QUOTES</literal> <optional> <literal>ON SCALAR STRING</literal> </optional> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <replaceable>value</replaceable>s. + This function must return a JSON string, so if the path expression + returns multiple SQL/JSON items, you must wrap the result using the + <literal>WITH WRAPPER</literal> clause. If the wrapper is + <literal>UNCONDITIONAL</literal>, an array wrapper will always + be applied, even if the returned value is already a single JSON object + or array, but if it is <literal>CONDITIONAL</literal>, it will not be + applied to a single array or object. <literal>UNCONDITIONAL</literal> + is the default. If the result is a scalar string, by default the value + returned will have surrounding quotes making it a valid JSON value, + which can be made explicit by specifying <literal>KEEP QUOTES</literal>. + Conversely, quotes can be omitted by specifying <literal>OMIT QUOTES</literal>. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>; + the default returned type is <type>text</type>. + The <literal>ON EMPTY</literal> clause specifies the behavior if the + <replaceable>path_expression</replaceable> yields no value at all; the + default when <literal>ON ERROR</literal> is not specified is to return a + null value. The <literal>ON ERROR</literal> clause specifies the behavior + if an error occurs as a result of <type>jsonpath</type> evaluation + (including cast to the output type) or during the execution of + <literal>ON EMPTY</literal> behavior (that was caused by empty result of + <type>jsonpath</type> evaluation); the default when + <literal>ON ERROR</literal> is not specified is to return a null value. + </para> + <para> + <literal>json_query(jsonb '[1,[2,3],null]', 'lax $[*][1]' WITH CONDITIONAL WRAPPER)</literal> + <returnvalue>[3]</returnvalue> + </para></entry> + </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm><primary>json_value</primary></indexterm> + <function>json_value</function> ( + <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> + <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional> + <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional> + <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>) + </para> + <para> + Returns the result of applying the + <replaceable>path_expression</replaceable> to the + <replaceable>context_item</replaceable> using the + <literal>PASSING</literal> <replaceable>value</replaceable>s. The + extracted value must be a single <acronym>SQL/JSON</acronym> scalar + item. For results that are objects or arrays, use the + <function>json_query</function> function instead. + The returned <replaceable>data_type</replaceable> has the same semantics + as for constructor functions like <function>json_objectagg</function>. + The default returned type is <type>text</type>. + The <literal>ON ERROR</literal> and <literal>ON EMPTY</literal> + clauses have similar semantics as mentioned in the description of + <function>json_query</function>. + </para> + <para> + <literal>json_value(jsonb '"123.45"', '$' RETURNING float)</literal> + <returnvalue>123.45</returnvalue> + </para> + <para> + <literal>json_value(jsonb '"03:04 2015-02-01"', '$.datetime("HH24:MI&nbsp;YYYY-MM-DD")' RETURNING date)</literal> + <returnvalue>2015-02-01</returnvalue> + </para> + <para> + <literal>json_value(jsonb '[1,2]', 'strict $[*]' DEFAULT 9 ON ERROR)</literal> + <returnvalue>9</returnvalue> + </para></entry> + </row> + </tbody> + </tgroup> + </table> + </sect2> <sect2 id="functions-sqljson-path"> diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 2c62b0c9c8..710f5af311 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -49,6 +49,7 @@ #include "utils/builtins.h" #include "utils/datum.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -88,6 +89,16 @@ static void ExecBuildAggTransCall(ExprState *state, AggState *aggstate, FunctionCallInfo fcinfo, AggStatePerTrans pertrans, int transno, int setno, int setoff, bool ishash, bool nullcheck); +static void ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch); +static Datum GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null); +static JsonCoercionState *ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull); +static JsonItemCoercionsState *ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull); /* @@ -2411,6 +2422,14 @@ ExecInitExprRec(Expr *node, ExprState *state, break; } + case T_JsonExpr: + { + JsonExpr *jexpr = castNode(JsonExpr, node); + + ExecInitJsonExpr(jexpr, state, resv, resnull, &scratch); + break; + } + case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -4178,3 +4197,413 @@ ExecBuildParamSetEqual(TupleDesc desc, return state; } + +/* + * Push steps to evaluate a JsonExpr and its various subsidiary expressions. + */ +static void +ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch) +{ + JsonExprState *jsestate = palloc0(sizeof(JsonExprState)); + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + ListCell *argexprlc; + ListCell *argnamelc; + int skip_step_off = -1; + int passing_args_step_off = -1; + int coercion_step_off = -1; + int coercion_finish_step_off = -1; + int behavior_step_off = -1; + int onempty_expr_step_off = -1; + int onempty_jump_step_off = -1; + int onerror_expr_step_off = -1; + int onerror_jump_step_off = -1; + List *adjust_jumps = NIL; + ListCell *lc; + ExprEvalStep *as; + + jsestate->jsexpr = jexpr; + + /* + * Add steps to compute formatted_expr, pathspec, and PASSING arg + * expressions as things that must be evaluated *before* the actual JSON + * path expression. + */ + ExecInitExprRec((Expr *) jexpr->formatted_expr, state, + &pre_eval->formatted_expr.value, + &pre_eval->formatted_expr.isnull); + ExecInitExprRec((Expr *) jexpr->path_spec, state, + &pre_eval->pathspec.value, + &pre_eval->pathspec.isnull); + + /* + * Before pushing steps for PASSING args, push a step to decide whether to + * skip evaluating the args and the JSON path expression depending on + * whether either of formatted_expr and pathspec is NULL; see + * ExecEvalJsonExprSkip(). + */ + scratch->opcode = EEOP_JSONEXPR_SKIP; + scratch->d.jsonexpr_skip.jsestate = jsestate; + skip_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* PASSING args. */ + jsestate->pre_eval.args = NIL; + passing_args_step_off = state->steps_len; + forboth(argexprlc, jexpr->passing_values, + argnamelc, jexpr->passing_names) + { + Expr *argexpr = (Expr *) lfirst(argexprlc); + String *argname = lfirst_node(String, argnamelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = pstrdup(argname->sval); + var->typid = exprType((Node *) argexpr); + var->typmod = exprTypmod((Node *) argexpr); + + ExecInitExprRec((Expr *) argexpr, state, &var->value, &var->isnull); + + pre_eval->args = lappend(pre_eval->args, var); + } + + /* Step for the actual JSON path evaluation; see ExecEvalJsonExpr(). */ + scratch->opcode = EEOP_JSONEXPR_PATH; + scratch->d.jsonexpr.jsestate = jsestate; + ExprEvalPushStep(state, scratch); + + /* + * Push steps to control the evaluation of expressions based on the result + * of JSON path evaluation. + */ + + /* + * Step to handle ON ERROR and ON EMPTY behavior; see + * ExecEvalJsonExprBehavior(). + */ + scratch->opcode = EEOP_JSONEXPR_BEHAVIOR; + scratch->d.jsonexpr_behavior.jsestate = jsestate; + behavior_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Step(s) to evaluate ON EMPTY expression */ + onempty_expr_step_off = state->steps_len; + if (jexpr->on_empty && + jexpr->on_empty->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_empty->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_empty->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_empty, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onempty_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + } + } + + /* Step(s) to evaluate ON ERROR expression */ + onerror_expr_step_off = state->steps_len; + if (jexpr->on_error && + jexpr->on_error->btype != JSON_BEHAVIOR_ERROR) + { + if (jexpr->on_error->default_expr) + { + ExecInitExprRec((Expr *) jexpr->on_error->default_expr, + state, resv, resnull); + + /* + * Emit JUMP step to jump to end of JsonExpr code, because the + * default expression has already been coerced, so there's nothing + * more to do. + */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + else + { + Datum constvalue; + bool constisnull; + + constvalue = GetJsonBehaviorValue(jexpr->on_error, &constisnull); + scratch->opcode = EEOP_CONST; + scratch->d.constval.value = constvalue; + scratch->d.constval.isnull = constisnull; + + ExprEvalPushStep(state, scratch); + + /* + * Emit JUMP step to jump to the coercion step to coerce the above + * value to the desired output type. + */ + onerror_jump_step_off = state->steps_len; + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* set later */ + ExprEvalPushStep(state, scratch); + } + } + + /* + * Step to handle applying coercion to the JSON item returned by + * EEOP_JSONEXPR_PATH or to the ON EMPTY/ERROR expression as + * EEOP_JSONEXPR_BEHAVIOR decides; see ExecEvalJsonExprCoercion(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* Initialize coercion expression(s). */ + if (jexpr->result_coercion) + { + jsestate->result_jcstate = + ExecInitJsonCoercion(scratch, state, jexpr->result_coercion, + resv, resnull); + /* See the comment above. */ + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* computed later */ + ExprEvalPushStep(state, scratch); + adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1); + } + if (jexpr->coercions) + { + /* + * ExecPrepareJsonItemCoercion() called by ExecEvalJsonExpr() chooses + * one for a given JSON item returned by JsonPathValue(). + */ + jsestate->item_jcstates = + ExecInitJsonItemCoercions(scratch, state, jexpr->coercions, + resv, resnull); + } + + /* + * And a step to clean up after the coercion step; see + * ExecEvalJsonExprCoercionFinish(). + */ + scratch->opcode = EEOP_JSONEXPR_COERCION_FINISH; + scratch->d.jsonexpr_coercion.jsestate = jsestate; + coercion_finish_step_off = state->steps_len; + ExprEvalPushStep(state, scratch); + + /* + * Adjust jump target addresses in various post-eval steps now that we + * have all the steps in place. + */ + + /* EEOP_JSONEXPR_SKIP */ + Assert(skip_step_off >= 0); + as = &state->steps[skip_step_off]; + as->d.jsonexpr_skip.jump_coercion = coercion_step_off; + as->d.jsonexpr_skip.jump_passing_args = passing_args_step_off; + + /* EEOP_JSONEXPR_BEHAVIOR */ + Assert(behavior_step_off >= 0); + as = &state->steps[behavior_step_off]; + as->d.jsonexpr_behavior.jump_onerror_expr = onerror_expr_step_off; + as->d.jsonexpr_behavior.jump_onempty_expr = onempty_expr_step_off; + as->d.jsonexpr_behavior.jump_coercion = coercion_step_off; + as->d.jsonexpr_behavior.jump_skip_coercion = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION */ + Assert(coercion_step_off >= 0); + as = &state->steps[coercion_step_off]; + as->d.jsonexpr_coercion.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JSONEXPR_COERCION_FINISH */ + Assert(coercion_finish_step_off >= 0); + as = &state->steps[coercion_finish_step_off]; + as->d.jsonexpr_coercion_finish.jump_coercion_error = behavior_step_off; + as->d.jsonexpr_coercion_finish.jump_coercion_done = coercion_finish_step_off + 1; + + /* EEOP_JUMP steps */ + + /* + * Ones after ON EMPTY and ON ERROR non-default expressions should jump to + * the coercion step. + */ + if (onempty_jump_step_off >= 0) + { + as = &state->steps[onempty_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + if (onerror_jump_step_off >= 0) + { + as = &state->steps[onerror_jump_step_off]; + as->d.jump.jumpdone = coercion_step_off; + } + + /* The rest should jump to the end. */ + foreach(lc, adjust_jumps) + { + as = &state->steps[lfirst_int(lc)]; + as->d.jump.jumpdone = state->steps_len; + } + + /* + * Set RETURNING type's input function used by ExecEvalJsonExprCoercion(). + */ + if (jexpr->omit_quotes || + (jexpr->result_coercion && jexpr->result_coercion->via_io)) + { + Oid typinput; + FmgrInfo *finfo; + + /* lookup the result type's input function */ + getTypeInputInfo(jexpr->returning->typid, &typinput, + &jsestate->input.typioparam); + finfo = palloc0(sizeof(FmgrInfo)); + fmgr_info(typinput, finfo); + jsestate->input.finfo = finfo; + } +} + +/* + * Evaluate a JSON error/empty behavior result. + */ +static Datum +GetJsonBehaviorValue(JsonBehavior *behavior, bool *is_null) +{ + *is_null = false; + + switch (behavior->btype) + { + case JSON_BEHAVIOR_EMPTY_ARRAY: + return JsonbPGetDatum(JsonbMakeEmptyArray()); + + case JSON_BEHAVIOR_EMPTY_OBJECT: + return JsonbPGetDatum(JsonbMakeEmptyObject()); + + case JSON_BEHAVIOR_TRUE: + return BoolGetDatum(true); + + case JSON_BEHAVIOR_FALSE: + return BoolGetDatum(false); + + case JSON_BEHAVIOR_NULL: + case JSON_BEHAVIOR_UNKNOWN: + case JSON_BEHAVIOR_EMPTY: + *is_null = true; + return (Datum) 0; + + case JSON_BEHAVIOR_DEFAULT: + /* Always handled in the caller. */ + Assert(false); + return (Datum) 0; + + default: + elog(ERROR, "unrecognized SQL/JSON behavior %d", behavior->btype); + return (Datum) 0; + } +} + +/* Initialize one JsonCoercion for execution. */ +static JsonCoercionState * +ExecInitJsonCoercion(ExprEvalStep *scratch, ExprState *state, + JsonCoercion *coercion, + Datum *resv, bool *resnull) +{ + JsonCoercionState *jcstate = palloc0(sizeof(JsonCoercionState)); + + jcstate->coercion = coercion; + if (coercion && coercion->expr) + { + Datum *save_innermost_caseval; + bool *save_innermost_casenull; + + jcstate->jump_eval_expr = state->steps_len; + + /* Push step(s) to compute cstate->coercion. */ + save_innermost_caseval = state->innermost_caseval; + save_innermost_casenull = state->innermost_casenull; + + state->innermost_caseval = resv; + state->innermost_casenull = resnull; + + ExecInitExprRec((Expr *) coercion->expr, state, resv, resnull); + + state->innermost_caseval = save_innermost_caseval; + state->innermost_casenull = save_innermost_casenull; + } + else + jcstate->jump_eval_expr = -1; + + return jcstate; +} + +/* + * Push steps to evaluate coercions from a given JsonItemCoercions, which + * contains all possible coercions that may need to be applied to JSON + * items coming from evaluating the parent JsonExpr. + */ +static JsonItemCoercionsState * +ExecInitJsonItemCoercions(ExprEvalStep *scratch, ExprState *state, + JsonItemCoercions *coercions, + Datum *resv, bool *resnull) +{ + JsonCoercion **coercion; + JsonItemCoercionsState *item_jcstates = palloc0(sizeof(JsonItemCoercionsState)); + JsonCoercionState **item_jcstate; + ExprEvalStep *as; + List *adjust_jumps = NIL; + ListCell *lc; + + /* Push the steps of individual coercions. */ + for (coercion = &coercions->null, + item_jcstate = &item_jcstates->null; + coercion <= &coercions->composite; + coercion++, item_jcstate++) + { + *item_jcstate = ExecInitJsonCoercion(scratch, state, *coercion, + resv, resnull); + + /* Emit JUMP step to skip past other coercions' steps. */ + scratch->opcode = EEOP_JUMP; + + /* + * Remember JUMP step address to set the actual jump target address + * below. + */ + adjust_jumps = lappend_int(adjust_jumps, state->steps_len); + ExprEvalPushStep(state, scratch); + } + + foreach(lc, adjust_jumps) + { + int jump_step_id = lfirst_int(lc); + + as = &state->steps[jump_step_id]; + as->d.jump.jumpdone = state->steps_len; + } + + return item_jcstates; +} diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 24c2b60c62..cf42d45c98 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -74,6 +74,7 @@ #include "utils/json.h" #include "utils/jsonb.h" #include "utils/jsonfuncs.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/timestamp.h" @@ -152,6 +153,9 @@ static TupleDesc get_cached_rowtype(Oid type_id, int32 typmod, bool *changed); static void ExecEvalRowNullInt(ExprState *state, ExprEvalStep *op, ExprContext *econtext, bool checkisnull); +static Datum ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate); /* fast-path evaluation functions */ static Datum ExecJustInnerVar(ExprState *state, ExprContext *econtext, bool *isnull); @@ -480,6 +484,11 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_XMLEXPR, &&CASE_EEOP_JSON_CONSTRUCTOR, &&CASE_EEOP_IS_JSON, + &&CASE_EEOP_JSONEXPR_SKIP, + &&CASE_EEOP_JSONEXPR_PATH, + &&CASE_EEOP_JSONEXPR_BEHAVIOR, + &&CASE_EEOP_JSONEXPR_COERCION, + &&CASE_EEOP_JSONEXPR_COERCION_FINISH, &&CASE_EEOP_AGGREF, &&CASE_EEOP_GROUPING_FUNC, &&CASE_EEOP_WINDOW_FUNC, @@ -1186,8 +1195,14 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) /* second and third arguments are already set up */ fcinfo_in->isnull = false; + + /* Pass down soft-error capture node if any. */ + fcinfo_in->context = state->escontext; + d = FunctionCallInvoke(fcinfo_in); *op->resvalue = d; + if (SOFT_ERROR_OCCURRED(state->escontext)) + *op->resnull = true; /* Should get null result if and only if str is NULL */ if (str == NULL) @@ -1195,7 +1210,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) Assert(*op->resnull); Assert(fcinfo_in->isnull); } - else + else if (!SOFT_ERROR_OCCURRED(state->escontext)) { Assert(!*op->resnull); Assert(!fcinfo_in->isnull); @@ -1543,6 +1558,38 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_JSONEXPR_PATH) + { + /* too complex for an inline implementation */ + ExecEvalJsonExpr(state, op, econtext); + EEO_NEXT(); + } + + EEO_CASE(EEOP_JSONEXPR_SKIP) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprSkip(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_BEHAVIOR) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprBehavior(state, op)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercion(state, op, econtext, + *op->resvalue, *op->resnull)); + } + + EEO_CASE(EEOP_JSONEXPR_COERCION_FINISH) + { + /* too complex for an inline implementation */ + EEO_JUMP(ExecEvalJsonExprCoercionFinish(state, op)); + } + EEO_CASE(EEOP_AGGREF) { /* @@ -3745,7 +3792,7 @@ ExecEvalConstraintCheck(ExprState *state, ExprEvalStep *op) { if (!*op->d.domaincheck.checknull && !DatumGetBool(*op->d.domaincheck.checkvalue)) - ereport(ERROR, + errsave(state->escontext, (errcode(ERRCODE_CHECK_VIOLATION), errmsg("value for domain %s violates check constraint \"%s\"", format_type_be(op->d.domaincheck.resulttype), @@ -4138,6 +4185,464 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op) *op->resvalue = BoolGetDatum(res); } +/* + * Evaluate given JsonExpr by performing the specified JSON operation. + * + * This also populates the JsonExprPostEvalState with the information needed + * by the subsequent steps that handle the specified JsonBehavior. + */ +void +ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) +{ + JsonExprState *jsestate = op->d.jsonexpr.jsestate; + JsonExprPreEvalState *pre_eval = &jsestate->pre_eval; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonExpr *jexpr = jsestate->jsexpr; + Datum item; + Datum res = (Datum) 0; + bool resnull = true; + JsonPath *path; + bool throw_error = (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR); + bool *error = &post_eval->error; + bool *empty = &post_eval->empty; + + item = pre_eval->formatted_expr.value; + path = DatumGetJsonPathP(pre_eval->pathspec.value); + + /* Reset JsonExprPostEvalState for this evaluation. */ + memset(post_eval, 0, sizeof(*post_eval)); + + switch (jexpr->op) + { + case JSON_EXISTS_OP: + { + bool exists = JsonPathExists(item, path, + !throw_error ? error : NULL, + pre_eval->args); + + if (*error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + resnull = false; + res = BoolGetDatum(exists); + break; + } + + case JSON_QUERY_OP: + res = JsonPathQuery(item, path, jexpr->wrapper, empty, + !throw_error ? error : NULL, + pre_eval->args); + if (*error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + resnull = !DatumGetPointer(res); + break; + + case JSON_VALUE_OP: + { + JsonbValue *jbv = JsonPathValue(item, path, empty, + !throw_error ? error : NULL, + pre_eval->args); + + if (*error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + if (!jbv) /* NULL or empty */ + { + resnull = true; + break; + } + + Assert(!*empty); + + resnull = false; + + /* coerce scalar item to the output type */ + if (jexpr->returning->typid == JSONOID || + jexpr->returning->typid == JSONBOID) + { + /* Use result coercion from json[b] to the output type */ + res = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + break; + } + + /* + * Error out if no cast exists to coerce SQL/JSON item to the + * the output type. + */ + Assert(post_eval->item_jcstate == NULL); + res = ExecPrepareJsonItemCoercion(jbv, + jsestate->item_jcstates, + &post_eval->item_jcstate); + if (post_eval->item_jcstate && + post_eval->item_jcstate->coercion && + (post_eval->item_jcstate->coercion->via_io || + post_eval->item_jcstate->coercion->via_populate)) + { + if (!throw_error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * Coercion via I/O means here that the cast to the target + * type simply does not exist. + */ + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE), + errmsg("SQL/JSON item cannot be cast to target type"))); + } + break; + } + + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* + * If the ON EMPTY behavior is to cause an error, do so here. Other + * behaviors will be handled by the caller. + */ + if (*empty) + { + Assert(jexpr->on_empty); /* it is not JSON_EXISTS */ + + if (jexpr->on_empty->btype == JSON_BEHAVIOR_ERROR) + { + if (!throw_error) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + ereport(ERROR, + (errcode(ERRCODE_NO_SQL_JSON_ITEM), + errmsg("no SQL/JSON item"))); + } + } + + *op->resvalue = res; + *op->resnull = resnull; +} + +/* + * Skip calling ExecEvalJson() on the given JsonExpr? + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_skip.jsestate; + + /* + * Skip if either of the input expressions has turned out to be NULL, + * though do execute domain checks for NULLs, which are handled by the + * coercion step. + */ + if (jsestate->pre_eval.formatted_expr.isnull || + jsestate->pre_eval.pathspec.isnull) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_skip.jump_coercion; + } + + /* + * Go evaluate the PASSING args if any and subsequently JSON path itself. + */ + return op->d.jsonexpr_skip.jump_passing_args; +} + +/* + * Returns the step address to perform the JsonBehavior applicable to + * the JSON item that resulted from evaluating the given JsonExpr. + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op) +{ + JsonExprState *jsestate = op->d.jsonexpr_behavior.jsestate; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonBehavior *behavior = NULL; + int jump_to = -1; + + if (post_eval->error || post_eval->coercion_error) + { + behavior = jsestate->jsexpr->on_error; + jump_to = op->d.jsonexpr_behavior.jump_onerror_expr; + } + else if (post_eval->empty) + { + behavior = jsestate->jsexpr->on_empty; + jump_to = op->d.jsonexpr_behavior.jump_onempty_expr; + } + else if (!post_eval->coercion_done) + { + /* + * If no error or the JSON item is not empty, directly go to the + * coercion step to coerce the item as is. + */ + return op->d.jsonexpr_behavior.jump_coercion; + } + + Assert(behavior); + + /* + * Set up for coercion step that will run to coerce a non-default behavior + * value. It should use result_coercion, if any. Errors that may occur + * should be thrown for JSON ops other than JSON_VALUE_OP. + */ + if (behavior->btype != JSON_BEHAVIOR_DEFAULT) + { + post_eval->item_jcstate = NULL; + post_eval->coercing_behavior_expr = true; + } + + Assert(jump_to >= 0); + return jump_to; +} + +/* + * Evaluate or return the step address to evaluate a coercion of a JSON item + * to the target type. The former if the coercion must be done right away by + * calling its input function or by calling json_populate_type(). + * + * Returns the step address to be performed next. + */ +int +ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull) +{ + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonExpr *jexpr = jsestate->jsexpr; + JsonExprPostEvalState *post_eval = &jsestate->post_eval; + JsonCoercionState *item_jcstate = post_eval->item_jcstate; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + + if (item_jcstate == NULL && + jsestate->jsexpr->op != JSON_EXISTS_OP) + { + ErrorSaveContext escontext = {T_ErrorSaveContext}; + Node *escontext_p; + JsonCoercion *coercion; + Jsonb *jb; + + /* + * For JSON_QUERY_OP, throw the errors that occur when coercing a + * non-default JsonBehavior expression. In all other cases, respect + * the ON ERROR clause. + */ + if (jexpr->on_error->btype != JSON_BEHAVIOR_ERROR && + (!post_eval->coercing_behavior_expr || jexpr->op != JSON_QUERY_OP)) + escontext_p = (Node *) &escontext; + else + escontext_p = NULL; + coercion = result_jcstate ? result_jcstate->coercion : NULL; + jb = resnull ? NULL : DatumGetJsonbP(res); + + if ((coercion && coercion->via_io) || + (jexpr->omit_quotes && !resnull && + JB_ROOT_IS_SCALAR(jb))) + { + /* strip quotes and call typinput function */ + char *str = resnull ? NULL : JsonbUnquote(jb); + bool type_is_domain; + + type_is_domain = (getBaseType(jexpr->returning->typid) != + jexpr->returning->typid); + + /* + * Catch errors only if the type is not a domain, because errors + * caused by a domain's constraint failure must be thrown right + * away. + */ + if (!InputFunctionCallSafe(jsestate->input.finfo, str, + jsestate->input.typioparam, + jexpr->returning->typmod, + !type_is_domain ? escontext_p : NULL, + op->resvalue)) + { + post_eval->coercion_error = true; + *op->resnull = true; + *op->resvalue = (Datum) 0; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + else if (coercion && coercion->via_populate) + { + *op->resvalue = json_populate_type(res, JSONBOID, + jexpr->returning->typid, + jexpr->returning->typmod, + &post_eval->cache, + econtext->ecxt_per_query_memory, + op->resnull, + escontext_p); + if (SOFT_ERROR_OCCURRED(escontext_p)) + { + post_eval->coercion_error = true; + *op->resvalue = (Datum) 0; + *op->resnull = true; + return op->d.jsonexpr_coercion.jump_coercion_error; + } + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; + } + } + + if (jexpr->on_error->btype != JSON_BEHAVIOR_ERROR) + { + post_eval->escontext.type = T_ErrorSaveContext; + state->escontext = (Node *) &post_eval->escontext; + } + + if (item_jcstate && item_jcstate->jump_eval_expr >= 0) + return item_jcstate->jump_eval_expr; + else if (result_jcstate && result_jcstate->jump_eval_expr >= 0) + return result_jcstate->jump_eval_expr; + + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion.jump_coercion_done; +} + +/* + * Checks if the coercion evaluation led to an error. If an error did occur, + * this returns the address of the step that handles the error, otherwise + * the step after the coercion step, which finishes the JsonExpr processing. + */ +int +ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op) +{ + JsonExprPostEvalState *post_eval = + &op->d.jsonexpr_coercion_finish.jsestate->post_eval; + + if (SOFT_ERROR_OCCURRED(state->escontext)) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + post_eval->coercion_error = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_error; + } + + /* Reset. */ + state->escontext = NULL; + post_eval->coercion_done = true; + return op->d.jsonexpr_coercion_finish.jump_coercion_done; +} + +/* + * Prepare SQL/JSON item coercion to the output type. Returned a datum of the + * corresponding SQL type and a pointer to the coercion state. + */ +static Datum +ExecPrepareJsonItemCoercion(JsonbValue *item, + JsonItemCoercionsState *item_jcstates, + JsonCoercionState **p_item_jcstate) +{ + JsonCoercionState *item_jcstate; + Datum res; + JsonbValue buf; + + if (item->type == jbvBinary && + JsonContainerIsScalar(item->val.binary.data)) + { + bool res PG_USED_FOR_ASSERTS_ONLY; + + res = JsonbExtractScalar(item->val.binary.data, &buf); + item = &buf; + Assert(res); + } + + /* get coercion state reference and datum of the corresponding SQL type */ + switch (item->type) + { + case jbvNull: + item_jcstate = item_jcstates->null; + res = (Datum) 0; + break; + + case jbvString: + item_jcstate = item_jcstates->string; + res = PointerGetDatum(cstring_to_text_with_len(item->val.string.val, + item->val.string.len)); + break; + + case jbvNumeric: + item_jcstate = item_jcstates->numeric; + res = NumericGetDatum(item->val.numeric); + break; + + case jbvBool: + item_jcstate = item_jcstates->boolean; + res = BoolGetDatum(item->val.boolean); + break; + + case jbvDatetime: + res = item->val.datetime.value; + switch (item->val.datetime.typid) + { + case DATEOID: + item_jcstate = item_jcstates->date; + break; + case TIMEOID: + item_jcstate = item_jcstates->time; + break; + case TIMETZOID: + item_jcstate = item_jcstates->timetz; + break; + case TIMESTAMPOID: + item_jcstate = item_jcstates->timestamp; + break; + case TIMESTAMPTZOID: + item_jcstate = item_jcstates->timestamptz; + break; + default: + elog(ERROR, "unexpected jsonb datetime type oid %u", + item->val.datetime.typid); + return (Datum) 0; + } + break; + + case jbvArray: + case jbvObject: + case jbvBinary: + item_jcstate = item_jcstates->composite; + res = JsonbPGetDatum(JsonbValueToJsonb(item)); + break; + + default: + elog(ERROR, "unexpected jsonb value type %d", item->type); + return (Datum) 0; + } + + *p_item_jcstate = item_jcstate; + + return res; +} + /* * ExecEvalGroupingFunc diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 00d7b8110b..4f155ab0f3 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -1860,6 +1860,256 @@ llvm_compile_expr(ExprState *state) LLVMBuildBr(b, opblocks[opno + 1]); break; + case EEOP_JSONEXPR_PATH: + build_EvalXFunc(b, mod, "ExecEvalJsonExpr", + v_state, op, v_econtext); + LLVMBuildBr(b, opblocks[opno + 1]); + break; + + case EEOP_JSONEXPR_SKIP: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprSkip() to decide if JSON path + * evaluation can be skipped. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprSkip"), + params, lengthof(params), ""); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_skip.jump_coercion, which signifies + * skipping of JSON path evaluation, else to the next step + * which must point to the steps to evaluate PASSING args, + * if any, or to the JSON path evaluation. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_skip.jump_coercion), + ""), + opblocks[op->d.jsonexpr_skip.jump_coercion], + opblocks[opno + 1]); + break; + } + + case EEOP_JSONEXPR_BEHAVIOR: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + LLVMBasicBlockRef b_jump_onerror_default; + + /* + * Call ExecEvalJsonExprBehavior() to decide if ON EMPTY + * or ON ERROR behavior must be invoked depending on what + * JSON path evaluation returned. This returns the step + * address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprBehavior"), + params, lengthof(params), ""); + + b_jump_onerror_default = + l_bb_before_v(opblocks[opno + 1], + "op.%d.jsonexpr_behavior_jump_onerror_default", opno); + + /* + * Jump to coercion step if the returned address is the + * same as jsonexpr_behavior.jump_coercion, else to the + * next block, one that checks whether to evaluate the ON + * ERROR default expression. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_coercion), + ""), + opblocks[op->d.jsonexpr_behavior.jump_coercion], + b_jump_onerror_default); + + /* + * Block that checks whether to evaluate the ON ERROR + * default expression. + * + * Jump to evaluate the ON ERROR default expression if the + * returned address is the same as + * jsonexpr_behavior.jump_onerror_default, else jump to + * evaluate the ON EMPTY default expression. + */ + LLVMPositionBuilderAtEnd(b, b_jump_onerror_default); + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_behavior.jump_onerror_expr), + ""), + opblocks[op->d.jsonexpr_behavior.jump_onerror_expr], + opblocks[op->d.jsonexpr_behavior.jump_onempty_expr]); + break; + } + case EEOP_JSONEXPR_COERCION: + { + JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate; + JsonItemCoercionsState *item_jcstates = jsestate->item_jcstates; + JsonCoercionState *result_jcstate = jsestate->result_jcstate; + LLVMValueRef v_ret; + LLVMValueRef params[5]; + + /* + * Call ExecEvalJsonExprCoercion() to evaluate appropriate + * coercion. This will return the step address to jump + * to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + params[2] = v_econtext; + params[3] = v_resvaluep; + params[4] = v_resnullp; + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercion"), + params, lengthof(params), ""); + + /* + * Jump to handle a coercion error if the returned address + * is the same as jsonexpr_coercion.jump_coercion_error, + * else to the step after coercion (coercion done!). + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + + if (item_jcstates) + { + JsonCoercionState **item_jcstate; + int n_coercions = (int) + (item_jcstates->composite - item_jcstates->null) + 1; + int i; + LLVMBasicBlockRef *b_coercions; + + + /* + * Will create a block for each coercion below to + * check whether to evaluate the coercion's expression + * if there's one or to skip to the end if not. + */ + b_coercions = palloc((n_coercions + 1) * sizeof(LLVMBasicBlockRef)); + for (i = 0; i < n_coercions + 1; i++) + b_coercions[i] = + l_bb_before_v(opblocks[opno + 1], + "op.%d.json_item_coercion.%d", + opno, i); + + /* Jump to check first coercion */ + LLVMBuildBr(b, b_coercions[0]); + + /* + * Add conditional branches for individual coercion's + * expressions + */ + for (item_jcstate = &item_jcstates->null, i = 0; + item_jcstate <= &item_jcstates->composite; + item_jcstate++, i++) + { + /* Block for this coercion */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + + /* + * Jump to evaluate the coercion's expression if + * the address returned is the same as this + * coercion's jump_eval_expr (that is, if it is + * valid), else check the next coercion's. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const((*item_jcstate)->jump_eval_expr), + ""), + (*item_jcstate)->jump_eval_expr >= 0 ? + opblocks[(*item_jcstate)->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + b_coercions[i + 1]); + } + + /* + * A placeholder block that the last coercion's block + * might jump to, which unconditionally jumps to end + * of coercions. + */ + LLVMPositionBuilderAtEnd(b, b_coercions[i]); + LLVMBuildBr(b, opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + + /* + * Jump to evaluate the result_coercion's expression if + * none of the above coercions matched, that is, if + * there's one. + */ + if (result_jcstate) + { + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(result_jcstate->jump_eval_expr), + ""), + result_jcstate->jump_eval_expr >= 0 ? + opblocks[result_jcstate->jump_eval_expr] : + opblocks[op->d.jsonexpr_coercion.jump_coercion_done], + opblocks[op->d.jsonexpr_coercion.jump_coercion_done]); + } + break; + } + + case EEOP_JSONEXPR_COERCION_FINISH: + { + LLVMValueRef params[2]; + LLVMValueRef v_ret; + + /* + * Call ExecEvalJsonExprCoercionFinish() to check whether + * an coercion error occurred, in which case we must jump + * to whatever step handles the error. This returns the + * step address to jump to. + */ + params[0] = v_state; + params[1] = l_ptr_const(op, l_ptr(StructExprEvalStep)); + v_ret = LLVMBuildCall(b, + llvm_pg_func(mod, "ExecEvalJsonExprCoercionFinish"), + params, lengthof(params), ""); + + /* + * Jump to the step that handles coercion error if the + * returned address is the same as + * jsonexpr_coercion_finish.jump_coercion_error, else to + * jsonexpr_coercion_finish.jump_coercion_done. + */ + LLVMBuildCondBr(b, + LLVMBuildICmp(b, + LLVMIntEQ, + v_ret, + l_int32_const(op->d.jsonexpr_coercion_finish.jump_coercion_error), + ""), + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_error], + opblocks[op->d.jsonexpr_coercion_finish.jump_coercion_done]); + break; + } + case EEOP_AGGREF: { LLVMValueRef v_aggno; diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c index 41ac4c6f45..cf3ced3427 100644 --- a/src/backend/jit/llvm/llvmjit_types.c +++ b/src/backend/jit/llvm/llvmjit_types.c @@ -135,6 +135,11 @@ void *referenced_functions[] = ExecEvalXmlExpr, ExecEvalJsonConstructor, ExecEvalJsonIsPredicate, + ExecEvalJsonExprSkip, + ExecEvalJsonExprBehavior, + ExecEvalJsonExprCoercion, + ExecEvalJsonExprCoercionFinish, + ExecEvalJsonExpr, MakeExpandedObjectReadOnlyInternal, slot_getmissingattrs, slot_getsomeattrs_int, diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 0e7e6e46d9..9c02634355 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -860,6 +860,21 @@ makeJsonValueExpr(Expr *raw_expr, Expr *formatted_expr, return jve; } +/* + * makeJsonBehavior - + * creates a JsonBehavior node + */ +JsonBehavior * +makeJsonBehavior(JsonBehaviorType type, Node *default_expr) +{ + JsonBehavior *behavior = makeNode(JsonBehavior); + + behavior->btype = type; + behavior->default_expr = default_expr; + + return behavior; +} + /* * makeJsonEncoding - * converts JSON encoding name to enum JsonEncoding diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index c03f4f23e2..cd5fff2bbe 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -234,6 +234,12 @@ exprType(const Node *expr) case T_JsonIsPredicate: type = BOOLOID; break; + case T_JsonExpr: + type = ((const JsonExpr *) expr)->returning->typid; + break; + case T_JsonCoercion: + type = exprType(((const JsonCoercion *) expr)->expr); + break; case T_NullTest: type = BOOLOID; break; @@ -493,6 +499,10 @@ exprTypmod(const Node *expr) return exprTypmod((Node *) ((const JsonValueExpr *) expr)->formatted_expr); case T_JsonConstructorExpr: return ((const JsonConstructorExpr *) expr)->returning->typmod; + case T_JsonExpr: + return ((JsonExpr *) expr)->returning->typmod; + case T_JsonCoercion: + return exprTypmod(((const JsonCoercion *) expr)->expr); case T_CoerceToDomain: return ((const CoerceToDomain *) expr)->resulttypmod; case T_CoerceToDomainValue: @@ -969,6 +979,22 @@ exprCollation(const Node *expr) /* IS JSON's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + coll = InvalidOid; + else if (coercion->expr) + coll = exprCollation(coercion->expr); + else if (coercion->via_io || coercion->via_populate) + coll = coercion->collation; + else + coll = InvalidOid; + } + break; + case T_NullTest: /* NullTest's result is boolean ... */ coll = InvalidOid; /* ... so it has no collation */ @@ -1205,6 +1231,21 @@ exprSetCollation(Node *expr, Oid collation) case T_JsonIsPredicate: Assert(!OidIsValid(collation)); /* result is always boolean */ break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) expr; + JsonCoercion *coercion = jexpr->result_coercion; + + if (!coercion) + Assert(!OidIsValid(collation)); + else if (coercion->expr) + exprSetCollation(coercion->expr, collation); + else if (coercion->via_io || coercion->via_populate) + coercion->collation = collation; + else + Assert(!OidIsValid(collation)); + } + break; case T_NullTest: /* NullTest's result is boolean ... */ Assert(!OidIsValid(collation)); /* ... so never set a collation */ @@ -1508,6 +1549,15 @@ exprLocation(const Node *expr) case T_JsonIsPredicate: loc = ((const JsonIsPredicate *) expr)->location; break; + case T_JsonExpr: + { + const JsonExpr *jsexpr = (const JsonExpr *) expr; + + /* consider both function name and leftmost arg */ + loc = leftmostLoc(jsexpr->location, + exprLocation(jsexpr->formatted_expr)); + } + break; case T_NullTest: { const NullTest *nexpr = (const NullTest *) expr; @@ -2260,6 +2310,54 @@ expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + if (WALK(jexpr->formatted_expr)) + return true; + if (WALK(jexpr->result_coercion)) + return true; + if (WALK(jexpr->passing_values)) + return true; + /* we assume walker doesn't care about passing_names */ + if (jexpr->on_empty && + WALK(jexpr->on_empty->default_expr)) + return true; + if (WALK(jexpr->on_error->default_expr)) + return true; + if (WALK(jexpr->coercions)) + return true; + } + break; + case T_JsonCoercion: + return WALK(((JsonCoercion *) node)->expr); + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + + if (WALK(coercions->null)) + return true; + if (WALK(coercions->string)) + return true; + if (WALK(coercions->numeric)) + return true; + if (WALK(coercions->boolean)) + return true; + if (WALK(coercions->date)) + return true; + if (WALK(coercions->time)) + return true; + if (WALK(coercions->timetz)) + return true; + if (WALK(coercions->timestamp)) + return true; + if (WALK(coercions->timestamptz)) + return true; + if (WALK(coercions->composite)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: @@ -3259,6 +3357,54 @@ expression_tree_mutator_impl(Node *node, return (Node *) newnode; } + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + JsonExpr *newnode; + + FLATCOPY(newnode, jexpr, JsonExpr); + MUTATE(newnode->path_spec, jexpr->path_spec, Node *); + MUTATE(newnode->formatted_expr, jexpr->formatted_expr, Node *); + MUTATE(newnode->result_coercion, jexpr->result_coercion, JsonCoercion *); + MUTATE(newnode->passing_values, jexpr->passing_values, List *); + /* assume mutator does not care about passing_names */ + if (newnode->on_empty) + MUTATE(newnode->on_empty->default_expr, + jexpr->on_empty->default_expr, Node *); + MUTATE(newnode->on_error->default_expr, + jexpr->on_error->default_expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonCoercion: + { + JsonCoercion *coercion = (JsonCoercion *) node; + JsonCoercion *newnode; + + FLATCOPY(newnode, coercion, JsonCoercion); + MUTATE(newnode->expr, coercion->expr, Node *); + return (Node *) newnode; + } + break; + case T_JsonItemCoercions: + { + JsonItemCoercions *coercions = (JsonItemCoercions *) node; + JsonItemCoercions *newnode; + + FLATCOPY(newnode, coercions, JsonItemCoercions); + MUTATE(newnode->null, coercions->null, JsonCoercion *); + MUTATE(newnode->string, coercions->string, JsonCoercion *); + MUTATE(newnode->numeric, coercions->numeric, JsonCoercion *); + MUTATE(newnode->boolean, coercions->boolean, JsonCoercion *); + MUTATE(newnode->date, coercions->date, JsonCoercion *); + MUTATE(newnode->time, coercions->time, JsonCoercion *); + MUTATE(newnode->timetz, coercions->timetz, JsonCoercion *); + MUTATE(newnode->timestamp, coercions->timestamp, JsonCoercion *); + MUTATE(newnode->timestamptz, coercions->timestamptz, JsonCoercion *); + MUTATE(newnode->composite, coercions->composite, JsonCoercion *); + return (Node *) newnode; + } + break; case T_NullTest: { NullTest *ntest = (NullTest *) node; @@ -3945,6 +4091,43 @@ raw_expression_tree_walker_impl(Node *node, break; case T_JsonIsPredicate: return WALK(((JsonIsPredicate *) node)->expr); + case T_JsonArgument: + return WALK(((JsonArgument *) node)->val); + case T_JsonCommon: + { + JsonCommon *jc = (JsonCommon *) node; + + if (WALK(jc->expr)) + return true; + if (WALK(jc->pathspec)) + return true; + if (WALK(jc->passing)) + return true; + } + break; + case T_JsonBehavior: + { + JsonBehavior *jb = (JsonBehavior *) node; + + if (jb->btype == JSON_BEHAVIOR_DEFAULT && + WALK(jb->default_expr)) + return true; + } + break; + case T_JsonFuncExpr: + { + JsonFuncExpr *jfe = (JsonFuncExpr *) node; + + if (WALK(jfe->common)) + return true; + if (jfe->output && WALK(jfe->output)) + return true; + if (WALK(jfe->on_empty)) + return true; + if (WALK(jfe->on_error)) + return true; + } + break; case T_NullTest: return WALK(((NullTest *) node)->arg); case T_BooleanTest: diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index d6ceafd51c..3bb48b64d5 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -4850,7 +4850,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context) IsA(node, SQLValueFunction) || IsA(node, XmlExpr) || IsA(node, CoerceToDomain) || - IsA(node, NextValueExpr)) + IsA(node, NextValueExpr) || + IsA(node, JsonExpr)) { /* Treat all these as having cost 1 */ context->total.per_tuple += cpu_operator_cost; diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index da258968b8..e40cfab4b7 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -53,6 +53,7 @@ #include "utils/fmgroids.h" #include "utils/json.h" #include "utils/jsonb.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/syscache.h" @@ -412,6 +413,24 @@ contain_mutable_functions_walker(Node *node, void *context) /* Check all subnodes */ } + if (IsA(node, JsonExpr)) + { + JsonExpr *jexpr = castNode(JsonExpr, node); + Const *cnst; + + if (!IsA(jexpr->path_spec, Const)) + return true; + + cnst = castNode(Const, jexpr->path_spec); + + Assert(cnst->consttype == JSONPATHOID); + if (cnst->constisnull) + return false; + + return jspIsMutable(DatumGetJsonPathP(cnst->constvalue), + jexpr->passing_names, jexpr->passing_values); + } + if (IsA(node, SQLValueFunction)) { /* all variants of SQLValueFunction are stable */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 15ece871a0..b867eddfff 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -278,6 +278,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MergeWhenClause *mergewhen; struct KeyActions *keyactions; struct KeyAction *keyaction; + JsonBehavior *jsbehavior; + JsonQuotes js_quotes; } %type <node> stmt toplevel_stmt schema_stmt routine_body_stmt @@ -650,14 +652,22 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_returning_clause_opt json_name_and_value json_aggregate_func + json_api_common_syntax + json_argument %type <list> json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt + json_arguments %type <ival> json_encoding_clause_opt json_predicate_type_constraint + json_wrapper_behavior %type <boolean> json_key_uniqueness_constraint_opt json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt +%type <jsbehavior> json_value_behavior + json_query_behavior + json_exists_behavior +%type <js_quotes> json_quotes_clause_opt /* @@ -695,7 +705,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT - COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT + COMMITTED COMPRESSION CONCURRENTLY CONDITIONAL CONFIGURATION CONFLICT CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY COST CREATE CROSS CSV CUBE CURRENT_P CURRENT_CATALOG CURRENT_DATE CURRENT_ROLE CURRENT_SCHEMA @@ -706,8 +716,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P DOUBLE_P DROP - EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT - EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION + EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE + EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION EXTENSION EXTERNAL EXTRACT FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR @@ -722,10 +732,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION - JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG - JSON_SCALAR JSON_SERIALIZE + JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE - KEY KEYS + KEY KEYS KEEP LABEL LANGUAGE LARGE_P LAST_P LATERAL_P LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL @@ -739,7 +749,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC - OBJECT_P OF OFF OFFSET OIDS OLD ON ONLY OPERATOR OPTION OPTIONS OR + OBJECT_P OF OFF OFFSET OIDS OLD OMIT ON ONLY OPERATOR OPTION OPTIONS OR ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER @@ -748,7 +758,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION - QUOTE + QUOTE QUOTES RANGE READ REAL REASSIGN RECHECK RECURSIVE REF_P REFERENCES REFERENCING REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA @@ -759,7 +769,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); SEQUENCE SEQUENCES SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P - START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P + START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRING_P STRIP_P SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN @@ -767,7 +777,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); TREAT TRIGGER TRIM TRUE_P TRUNCATE TRUSTED TYPE_P TYPES_P - UESCAPE UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN + UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING @@ -15686,6 +15696,192 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->quotes = $6; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->quotes = $6; + n->on_empty = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->quotes = $6; + n->on_error = $7; + n->location = @1; + $$ = (Node *) n; + } + | JSON_QUERY '(' + json_api_common_syntax + json_returning_clause_opt + json_wrapper_behavior + json_quotes_clause_opt + json_query_behavior ON EMPTY_P + json_query_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_QUERY_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->wrapper = $5; + if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@6))); + n->quotes = $6; + n->on_empty = $7; + n->on_error = $10; + n->location = @1; + $$ = (Node *) n; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->location = @1; + $$ = (Node *) p; + } + | JSON_EXISTS '(' + json_api_common_syntax + json_returning_clause_opt + json_exists_behavior ON ERROR_P + ')' + { + JsonFuncExpr *p = makeNode(JsonFuncExpr); + + p->op = JSON_EXISTS_OP; + p->common = (JsonCommon *) $3; + p->output = (JsonOutput *) $4; + p->on_error = $5; + p->location = @1; + $$ = (Node *) p; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + + | JSON_VALUE '(' + json_api_common_syntax + json_returning_clause_opt + json_value_behavior ON EMPTY_P + json_value_behavior ON ERROR_P + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + + n->op = JSON_VALUE_OP; + n->common = (JsonCommon *) $3; + n->output = (JsonOutput *) $4; + n->on_empty = $5; + n->on_error = $8; + n->location = @1; + $$ = (Node *) n; + } ; @@ -16412,6 +16608,72 @@ opt_asymmetric: ASYMMETRIC ; /* SQL/JSON support */ +json_api_common_syntax: + json_value_expr ',' a_expr /* i.e. a json_path */ + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = NULL; + n->location = @1; + $$ = (Node *) n; + } + + | json_value_expr ',' a_expr /* i.e. a json_path */ + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = NULL; + n->passing = $5; + n->location = @1; + $$ = (Node *) n; + } + | json_value_expr ',' a_expr AS name + PASSING json_arguments + { + JsonCommon *n = makeNode(JsonCommon); + + n->expr = (JsonValueExpr *) $1; + n->pathspec = $3; + n->pathname = $5; + n->passing = $7; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_arguments: + json_argument { $$ = list_make1($1); } + | json_arguments ',' json_argument { $$ = lappend($1, $3); } + ; + +json_argument: + json_value_expr AS ColLabel + { + JsonArgument *n = makeNode(JsonArgument); + + n->val = (JsonValueExpr *) $1; + n->name = $3; + $$ = (Node *) n; + } + ; + json_value_expr: a_expr json_format_clause_opt { @@ -16437,6 +16699,50 @@ json_encoding_clause_opt: | /* EMPTY */ { $$ = JS_ENC_DEFAULT; } ; +json_query_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + | EMPTY_P ARRAY { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + | EMPTY_P OBJECT_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + /* non-standard, for Oracle compatibility only */ + | EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } + ; + +json_exists_behavior: + ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | TRUE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_TRUE, NULL); } + | FALSE_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_FALSE, NULL); } + | UNKNOWN { $$ = makeJsonBehavior(JSON_BEHAVIOR_UNKNOWN, NULL); } + ; + +json_value_behavior: + NULL_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL); } + | ERROR_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); } + | DEFAULT a_expr { $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2); } + ; + +/* ARRAY is a noise word */ +json_wrapper_behavior: + WITHOUT WRAPPER { $$ = JSW_NONE; } + | WITHOUT ARRAY WRAPPER { $$ = JSW_NONE; } + | WITH WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL ARRAY WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; } + | WITH CONDITIONAL WRAPPER { $$ = JSW_CONDITIONAL; } + | WITH UNCONDITIONAL WRAPPER { $$ = JSW_UNCONDITIONAL; } + | /* empty */ { $$ = JSW_NONE; } + ; + +json_quotes_clause_opt: + KEEP QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_KEEP; } + | KEEP QUOTES { $$ = JS_QUOTES_KEEP; } + | OMIT QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_OMIT; } + | OMIT QUOTES { $$ = JS_QUOTES_OMIT; } + | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; } + ; + json_returning_clause_opt: RETURNING Typename json_format_clause_opt { @@ -17039,6 +17345,7 @@ unreserved_keyword: | COMMIT | COMMITTED | COMPRESSION + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17075,10 +17382,12 @@ unreserved_keyword: | DOUBLE_P | DROP | EACH + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17128,6 +17437,7 @@ unreserved_keyword: | INSTEAD | INVOKER | ISOLATION + | KEEP | KEY | KEYS | LABEL @@ -17174,6 +17484,7 @@ unreserved_keyword: | OFF | OIDS | OLD + | OMIT | OPERATOR | OPTION | OPTIONS @@ -17204,6 +17515,7 @@ unreserved_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REASSIGN @@ -17263,6 +17575,7 @@ unreserved_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUPPORT @@ -17285,6 +17598,7 @@ unreserved_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNKNOWN | UNLISTEN @@ -17345,10 +17659,13 @@ col_name_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE | LEAST | NATIONAL | NCHAR @@ -17581,6 +17898,7 @@ bare_label_keyword: | COMMITTED | COMPRESSION | CONCURRENTLY + | CONDITIONAL | CONFIGURATION | CONFLICT | CONNECTION @@ -17633,11 +17951,13 @@ bare_label_keyword: | DROP | EACH | ELSE + | EMPTY_P | ENABLE_P | ENCODING | ENCRYPTED | END_P | ENUM_P + | ERROR_P | ESCAPE | EVENT | EXCLUDE @@ -17707,10 +18027,14 @@ bare_label_keyword: | JSON | JSON_ARRAY | JSON_ARRAYAGG + | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG + | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_VALUE + | KEEP | KEY | KEYS | LABEL @@ -17771,6 +18095,7 @@ bare_label_keyword: | OFF | OIDS | OLD + | OMIT | ONLY | OPERATOR | OPTION @@ -17808,6 +18133,7 @@ bare_label_keyword: | PROGRAM | PUBLICATION | QUOTE + | QUOTES | RANGE | READ | REAL @@ -17876,6 +18202,7 @@ bare_label_keyword: | STORAGE | STORED | STRICT_P + | STRING_P | STRIP_P | SUBSCRIPTION | SUBSTRING @@ -17910,6 +18237,7 @@ bare_label_keyword: | UESCAPE | UNBOUNDED | UNCOMMITTED + | UNCONDITIONAL | UNENCRYPTED | UNIQUE | UNKNOWN diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c index 9f6afc351c..692e5d1225 100644 --- a/src/backend/parser/parse_collate.c +++ b/src/backend/parser/parse_collate.c @@ -691,6 +691,13 @@ assign_collations_walker(Node *node, assign_collations_context *context) &loccontext); } break; + case T_JsonExpr: + + /* + * Context item and PASSING arguments are already + * marked with collations in parse_expr.c. + */ + break; default: /* diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index fed8e4d089..180188f4aa 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -90,6 +90,7 @@ static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr); static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr); static Node *transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr *expr); +static Node *transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *p); static Node *make_row_comparison_op(ParseState *pstate, List *opname, List *largs, List *rargs, int location); static Node *make_row_distinct_op(ParseState *pstate, List *opname, @@ -353,6 +354,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr); break; + case T_JsonFuncExpr: + result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -3229,7 +3234,7 @@ makeJsonByteaToTextConversion(Node *expr, JsonFormat *format, int location) static Node * transformJsonValueExpr(ParseState *pstate, const char *constructName, JsonValueExpr *ve, JsonFormatType default_format, - Oid targettype) + Oid targettype, bool isarg) { Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr); Node *rawexpr; @@ -3266,6 +3271,35 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, else format = ve->format->format_type; } + else if (isarg) + { + /* Pass SQL/JSON item types directly without conversion to json[b]. */ + switch (exprtype) + { + case TEXTOID: + case NUMERICOID: + case BOOLOID: + case INT2OID: + case INT4OID: + case INT8OID: + case FLOAT4OID: + case FLOAT8OID: + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + return expr; + + default: + if (typcategory == TYPCATEGORY_STRING) + return expr; + /* else convert argument to json[b] type */ + break; + } + + format = default_format; + } else if (exprtype == JSONOID || exprtype == JSONBOID) format = JS_FORMAT_DEFAULT; /* do not format json[b] types */ else @@ -3277,7 +3311,8 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName, Node *coerced; bool only_allow_cast = OidIsValid(targettype); - if (!only_allow_cast && + if (!isarg && + !only_allow_cast && exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING) ereport(ERROR, errcode(ERRCODE_DATATYPE_MISMATCH), @@ -3626,7 +3661,7 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) Node *val = transformJsonValueExpr(pstate, "JSON_OBJECT()", kv->value, JS_FORMAT_DEFAULT, - InvalidOid); + InvalidOid, false); args = lappend(args, key); args = lappend(args, val); @@ -3813,7 +3848,7 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg) val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()", agg->arg->value, JS_FORMAT_DEFAULT, - InvalidOid); + InvalidOid, false); args = list_make2(key, val); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, @@ -3869,9 +3904,8 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg) Oid aggfnoid; Oid aggtype; - arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", - agg->arg, - JS_FORMAT_DEFAULT, InvalidOid); + arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", agg->arg, + JS_FORMAT_DEFAULT, InvalidOid, false); returning = transformJsonConstructorOutput(pstate, agg->constructor->output, list_make1(arg)); @@ -3918,9 +3952,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor) { JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc)); Node *val = transformJsonValueExpr(pstate, "JSON_ARRAY()", - jsval, - JS_FORMAT_DEFAULT, - InvalidOid); + jsval, JS_FORMAT_DEFAULT, + InvalidOid, false); args = lappend(args, val); } @@ -4079,7 +4112,7 @@ transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr) * function-like CASTs. */ arg = transformJsonValueExpr(pstate, "JSON()", jsexpr->expr, - JS_FORMAT_JSON, returning->typid); + JS_FORMAT_JSON, returning->typid, false); } return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL, @@ -4124,7 +4157,7 @@ transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr *expr) Node *arg = transformJsonValueExpr(pstate, "JSON_SERIALIZE()", expr->expr, JS_FORMAT_JSON, - InvalidOid); + InvalidOid, false); if (expr->output) { @@ -4158,3 +4191,463 @@ transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr *expr) return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg), NULL, returning, false, false, expr->location); } + +/* + * Transform a JSON PASSING clause. + */ +static void +transformJsonPassingArgs(ParseState *pstate, char *constructName, + JsonFormatType format, List *args, + List **passing_values, List **passing_names) +{ + ListCell *lc; + + *passing_values = NIL; + *passing_names = NIL; + + foreach(lc, args) + { + JsonArgument *arg = castNode(JsonArgument, lfirst(lc)); + Node *expr = transformJsonValueExpr(pstate, constructName, + arg->val, format, + InvalidOid, true); + + assign_expr_collations(pstate, expr); + + *passing_values = lappend(*passing_values, expr); + *passing_names = lappend(*passing_names, makeString(arg->name)); + } +} + +/* + * Transform a JSON BEHAVIOR clause. + */ +static JsonBehavior * +transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior, + JsonBehaviorType default_behavior) +{ + JsonBehaviorType behavior_type = default_behavior; + Node *default_expr = NULL; + + if (behavior) + { + behavior_type = behavior->btype; + if (behavior_type == JSON_BEHAVIOR_DEFAULT) + default_expr = transformExprRecurse(pstate, behavior->default_expr); + } + return makeJsonBehavior(behavior_type, default_expr); +} + +/* + * Common code for JSON_VALUE, JSON_QUERY, JSON_EXISTS transformation + * into a JsonExpr node. + */ +static JsonExpr * +transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = makeNode(JsonExpr); + Node *pathspec; + JsonFormatType format; + char *constructName; + + if (func->common->pathname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("JSON_TABLE path name is not allowed here"), + parser_errposition(pstate, func->location))); + + jsexpr->location = func->location; + jsexpr->op = func->op; + switch (jsexpr->op) + { + case JSON_EXISTS_OP: + constructName = "JSON_EXISTS()"; + break; + case JSON_QUERY_OP: + constructName = "JSON_QUERY()"; + break; + case JSON_VALUE_OP: + constructName = "JSON_VALUE()"; + break; + default: + elog(ERROR, "unrecognized SQL/JSON expression op %d", jsexpr->op); + break; + } + jsexpr->formatted_expr = transformJsonValueExpr(pstate, constructName, + func->common->expr, + JS_FORMAT_DEFAULT, + InvalidOid, false); + + assign_expr_collations(pstate, jsexpr->formatted_expr); + + /* format is determined by context item type */ + format = exprType(jsexpr->formatted_expr) == JSONBOID ? JS_FORMAT_JSONB : JS_FORMAT_JSON; + + jsexpr->result_coercion = NULL; + jsexpr->omit_quotes = false; + + jsexpr->format = func->common->expr->format; + + pathspec = transformExprRecurse(pstate, func->common->pathspec); + + jsexpr->path_spec = + coerce_to_target_type(pstate, pathspec, exprType(pathspec), + JSONPATHOID, -1, + COERCION_EXPLICIT, COERCE_IMPLICIT_CAST, + exprLocation(pathspec)); + if (!jsexpr->path_spec) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("JSON path expression must be type %s, not type %s", + "jsonpath", format_type_be(exprType(pathspec))), + parser_errposition(pstate, exprLocation(pathspec)))); + + /* transform and coerce to json[b] passing arguments */ + transformJsonPassingArgs(pstate, constructName, format, + func->common->passing, + &jsexpr->passing_values, + &jsexpr->passing_names); + + if (func->op != JSON_EXISTS_OP) + jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, + JSON_BEHAVIOR_NULL); + + if (func->op == JSON_EXISTS_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_FALSE); + else + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_NULL); + + return jsexpr; +} + +/* + * Assign default JSON returning type from the specified format or from + * the context item type. + */ +static void +assignDefaultJsonReturningType(Node *context_item, JsonFormat *context_format, + JsonReturning *ret) +{ + bool is_jsonb; + + ret->format = copyObject(context_format); + + if (ret->format->format_type == JS_FORMAT_DEFAULT) + is_jsonb = exprType(context_item) == JSONBOID; + else + is_jsonb = ret->format->format_type == JS_FORMAT_JSONB; + + ret->typid = is_jsonb ? JSONBOID : JSONOID; + ret->typmod = -1; +} + +/* + * Try to coerce expression to the output type or + * use json_populate_type() for composite, array and domain types or + * use coercion via I/O. + */ +static JsonCoercion * +coerceJsonExpr(ParseState *pstate, Node *expr, const JsonReturning *returning) +{ + char typtype; + JsonCoercion *coercion = makeNode(JsonCoercion); + + coercion->expr = coerceJsonFuncExpr(pstate, expr, returning, false); + + if (coercion->expr) + { + if (coercion->expr == expr) + coercion->expr = NULL; + + return coercion; + } + + typtype = get_typtype(returning->typid); + + if (returning->typid == RECORDOID || + typtype == TYPTYPE_COMPOSITE || + typtype == TYPTYPE_DOMAIN || + type_is_array(returning->typid)) + coercion->via_populate = true; + else + coercion->via_io = true; + + return coercion; +} + +/* + * Transform a JSON output clause of JSON_VALUE and JSON_QUERY. + */ +static void +transformJsonFuncExprOutput(ParseState *pstate, JsonFuncExpr *func, + JsonExpr *jsexpr) +{ + Node *expr = jsexpr->formatted_expr; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + /* JSON_VALUE returns text by default */ + if (func->op == JSON_VALUE_OP && !OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = TEXTOID; + jsexpr->returning->typmod = -1; + } + + if (OidIsValid(jsexpr->returning->typid)) + { + JsonReturning ret; + + if (func->op == JSON_VALUE_OP && + jsexpr->returning->typid != JSONOID && + jsexpr->returning->typid != JSONBOID) + { + /* Forced coercion via I/O for JSON_VALUE for non-JSON types */ + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = NULL; + jsexpr->result_coercion->via_io = true; + return; + } + + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, &ret); + + if (ret.typid != jsexpr->returning->typid || + ret.typmod != jsexpr->returning->typmod) + { + CaseTestExpr *cte = makeNode(CaseTestExpr); + + cte->typeId = exprType(expr); + cte->typeMod = exprTypmod(expr); + cte->collation = exprCollation(expr); + + Assert(cte->typeId == ret.typid); + Assert(cte->typeMod == ret.typmod); + + jsexpr->result_coercion = coerceJsonExpr(pstate, (Node *) cte, + jsexpr->returning); + } + } + else + assignDefaultJsonReturningType(jsexpr->formatted_expr, jsexpr->format, + jsexpr->returning); +} + +/* + * Coerce an expression in JSON DEFAULT behavior to the target output type. + */ +static Node * +coerceDefaultJsonExpr(ParseState *pstate, JsonExpr *jsexpr, Node *defexpr) +{ + int location; + Oid exprtype; + + if (!defexpr) + return NULL; + + exprtype = exprType(defexpr); + location = exprLocation(defexpr); + + if (location < 0) + location = jsexpr->location; + + defexpr = coerce_to_target_type(pstate, + defexpr, + exprtype, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!defexpr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast DEFAULT expression type %s to %s", + format_type_be(exprtype), + format_type_be(jsexpr->returning->typid)), + parser_errposition(pstate, location))); + + return defexpr; +} + +/* + * Initialize SQL/JSON item coercion from the SQL type "typid" to the target + * "returning" type. + */ +static JsonCoercion * +initJsonItemCoercion(ParseState *pstate, Oid typid, + const JsonReturning *returning) +{ + Node *expr; + + if (typid == UNKNOWNOID) + { + expr = (Node *) makeNullConst(UNKNOWNOID, -1, InvalidOid); + } + else + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + + placeholder->typeId = typid; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + expr = (Node *) placeholder; + } + + return coerceJsonExpr(pstate, expr, returning); +} + +static void +initJsonItemCoercions(ParseState *pstate, JsonItemCoercions *coercions, + const JsonReturning *returning, Oid contextItemTypeId) +{ + struct + { + JsonCoercion **coercion; + Oid typid; + } *p, + coercionTypids[] = + { + {&coercions->null, UNKNOWNOID}, + {&coercions->string, TEXTOID}, + {&coercions->numeric, NUMERICOID}, + {&coercions->boolean, BOOLOID}, + {&coercions->date, DATEOID}, + {&coercions->time, TIMEOID}, + {&coercions->timetz, TIMETZOID}, + {&coercions->timestamp, TIMESTAMPOID}, + {&coercions->timestamptz, TIMESTAMPTZOID}, + {&coercions->composite, contextItemTypeId}, + {NULL, InvalidOid} + }; + + for (p = coercionTypids; p->coercion; p++) + *p->coercion = initJsonItemCoercion(pstate, p->typid, returning); +} + +/* + * Transform JSON_VALUE, JSON_QUERY, JSON_EXISTS functions into a JsonExpr node. + */ +static Node * +transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) +{ + JsonExpr *jsexpr = transformJsonExprCommon(pstate, func); + const char *func_name = NULL; + Node *contextItemExpr = jsexpr->formatted_expr; + + /* + * Disallow FORMAT specification in the RETURNING clause of JSON_EXISTS() + * and JSON_VALUE(). + */ + if (func->output && + (func->op == JSON_VALUE_OP || func->op == JSON_EXISTS_OP)) + { + JsonFormat *format = func->output->returning->format; + + if (format->format_type != JS_FORMAT_DEFAULT || + format->encoding != JS_ENC_DEFAULT) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot specify FORMAT in RETURNING clause of %s", + func->op == JSON_VALUE_OP ? "JSON_VALUE()" : + "JSON_EXISTS()"), + parser_errposition(pstate, format->location))); + } + + switch (func->op) + { + case JSON_EXISTS_OP: + func_name = "JSON_EXISTS"; + + jsexpr->returning = transformJsonOutput(pstate, func->output, false); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + if (!OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = BOOLOID; + jsexpr->returning->typmod = -1; + } + else if (jsexpr->returning->typid != BOOLOID) + { + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + int location = exprLocation((Node *) jsexpr); + + placeholder->typeId = BOOLOID; + placeholder->typeMod = -1; + placeholder->collation = InvalidOid; + + jsexpr->result_coercion = makeNode(JsonCoercion); + jsexpr->result_coercion->expr = + coerce_to_target_type(pstate, (Node *) placeholder, BOOLOID, + jsexpr->returning->typid, + jsexpr->returning->typmod, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + location); + + if (!jsexpr->result_coercion->expr) + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(BOOLOID), + format_type_be(jsexpr->returning->typid)), + parser_coercion_errposition(pstate, location, (Node *) jsexpr))); + + if (jsexpr->result_coercion->expr == (Node *) placeholder) + jsexpr->result_coercion->expr = NULL; + } + break; + + case JSON_QUERY_OP: + func_name = "JSON_QUERY"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->wrapper = func->wrapper; + jsexpr->omit_quotes = (func->quotes == JS_QUOTES_OMIT); + break; + + case JSON_VALUE_OP: + func_name = "JSON_VALUE"; + + transformJsonFuncExprOutput(pstate, func, jsexpr); + + jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT; + jsexpr->returning->format->encoding = JS_ENC_DEFAULT; + + jsexpr->on_empty->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_empty->default_expr); + + jsexpr->on_error->default_expr = + coerceDefaultJsonExpr(pstate, jsexpr, + jsexpr->on_error->default_expr); + + jsexpr->coercions = makeNode(JsonItemCoercions); + initJsonItemCoercions(pstate, jsexpr->coercions, jsexpr->returning, + exprType(contextItemExpr)); + + break; + } + + if (exprType(contextItemExpr) != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("%s() is not yet implemented for the json type", func_name), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + return (Node *) jsexpr; +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 57247de363..21bdb2815d 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1982,6 +1982,21 @@ FigureColnameInternal(Node *node, char **name) /* make JSON_ARRAYAGG act like a regular function */ *name = "json_arrayagg"; return 2; + case T_JsonFuncExpr: + /* make SQL/JSON functions act like a regular function */ + switch (((JsonFuncExpr *) node)->op) + { + case JSON_EXISTS_OP: + *name = "json_exists"; + return 2; + case JSON_QUERY_OP: + *name = "json_query"; + return 2; + case JSON_VALUE_OP: + *name = "json_value"; + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index e27ea8ef97..188b5594e0 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -4426,6 +4426,50 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, } } +/* + * Parses the datetime format string in 'fmt_str' and returns true if it + * contains a timezone specifier, false if not. + */ +bool +datetime_format_has_tz(const char *fmt_str) +{ + bool incache; + int fmt_len = strlen(fmt_str); + int result; + FormatNode *format; + + if (fmt_len > DCH_CACHE_SIZE) + { + /* + * Allocate new memory if format picture is bigger than static cache + * and do not use cache (call parser always) + */ + incache = false; + + format = (FormatNode *) palloc((fmt_len + 1) * sizeof(FormatNode)); + + parse_format(format, fmt_str, DCH_keywords, + DCH_suff, DCH_index, DCH_FLAG, NULL); + } + else + { + /* + * Use cache buffers + */ + DCHCacheEntry *ent = DCH_cache_fetch(fmt_str, false); + + incache = true; + format = ent->format; + } + + result = DCH_datetime_type(format); + + if (!incache) + pfree(format); + + return result & DCH_ZONED; +} + /* * do_to_timestamp: shared code for to_timestamp and to_date * diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index 9781852b0c..ea5b386f8c 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -2162,3 +2162,65 @@ jsonb_float8(PG_FUNCTION_ARGS) PG_RETURN_DATUM(retValue); } + +/* + * Construct an empty array jsonb. + */ +Jsonb * +JsonbMakeEmptyArray(void) +{ + JsonbValue jbv; + + jbv.type = jbvArray; + jbv.val.array.elems = NULL; + jbv.val.array.nElems = 0; + jbv.val.array.rawScalar = false; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Construct an empty object jsonb. + */ +Jsonb * +JsonbMakeEmptyObject(void) +{ + JsonbValue jbv; + + jbv.type = jbvObject; + jbv.val.object.pairs = NULL; + jbv.val.object.nPairs = 0; + + return JsonbValueToJsonb(&jbv); +} + +/* + * Convert jsonb to a C-string stripping quotes from scalar strings. + */ +char * +JsonbUnquote(Jsonb *jb) +{ + if (JB_ROOT_IS_SCALAR(jb)) + { + JsonbValue v; + + (void) JsonbExtractScalar(&jb->root, &v); + + if (v.type == jbvString) + return pnstrdup(v.val.string.val, v.val.string.len); + else if (v.type == jbvBool) + return pstrdup(v.val.boolean ? "true" : "false"); + else if (v.type == jbvNumeric) + return DatumGetCString(DirectFunctionCall1(numeric_out, + PointerGetDatum(v.val.numeric))); + else if (v.type == jbvNull) + return pstrdup("null"); + else + { + elog(ERROR, "unrecognized jsonb value type %d", v.type); + return NULL; + } + } + else + return JsonbToCString(NULL, &jb->root, VARSIZE(jb)); +} diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index a4bfa5e404..2d5fa285fc 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -265,6 +265,7 @@ typedef struct PopulateArrayContext int *dims; /* dimensions */ int *sizes; /* current dimension counters */ int ndims; /* number of dimensions */ + Node *escontext; /* For soft-error capture */ } PopulateArrayContext; /* state for populate_array_json() */ @@ -442,12 +443,13 @@ static void JsValueToJsObject(JsValue *jsv, JsObject *jso); static Datum populate_composite(CompositeIOData *io, Oid typid, const char *colname, MemoryContext mcxt, HeapTupleHeader defaultval, JsValue *jsv, bool isnull); -static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv); +static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext); static void prepare_column_cache(ColumnIOData *column, Oid typid, int32 typmod, MemoryContext mcxt, bool need_scalar); static Datum populate_record_field(ColumnIOData *col, Oid typid, int32 typmod, const char *colname, MemoryContext mcxt, Datum defaultval, - JsValue *jsv, bool *isnull); + JsValue *jsv, bool *isnull, Node *escontext); static RecordIOData *allocate_record_info(MemoryContext mcxt, int ncolumns); static bool JsObjectGetField(JsObject *obj, char *field, JsValue *jsv); static void populate_recordset_record(PopulateRecordsetState *state, JsObject *obj); @@ -459,7 +461,8 @@ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims); static void populate_array_check_dimension(PopulateArrayContext *ctx, int ndim); static void populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv); static Datum populate_array(ArrayIOData *aio, const char *colname, - MemoryContext mcxt, JsValue *jsv); + MemoryContext mcxt, JsValue *jsv, Node *escontext, + bool *isnull); static Datum populate_domain(DomainIOData *io, Oid typid, const char *colname, MemoryContext mcxt, JsValue *jsv, bool isnull); @@ -2484,12 +2487,12 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) if (ndim <= 0) { if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the value of key \"%s\".", ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"))); } @@ -2506,13 +2509,13 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) appendStringInfo(&indices, "[%d]", ctx->sizes[i]); if (ctx->colname) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s of key \"%s\".", indices.data, ctx->colname))); else - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected JSON array"), errhint("See the array element %s.", @@ -2520,7 +2523,11 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim) } } -/* set the number of dimensions of the populated array when it becomes known */ +/* + * Set the number of dimensions of the populated array when it becomes known. + * + * Returns without doing anything if the input (ndims) is erratic. + */ static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) { @@ -2531,6 +2538,10 @@ populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) if (ndims <= 0) populate_array_report_expected_array(ctx, ndims); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + ctx->ndims = ndims; ctx->dims = palloc(sizeof(int) * ndims); ctx->sizes = palloc0(sizeof(int) * ndims); @@ -2548,12 +2559,16 @@ populate_array_check_dimension(PopulateArrayContext *ctx, int ndim) if (ctx->dims[ndim] == -1) ctx->dims[ndim] = dim; /* assign dimension if not yet known */ else if (ctx->dims[ndim] != dim) - ereport(ERROR, + errsave(ctx->escontext, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("malformed JSON array"), errdetail("Multidimensional arrays must have " "sub-arrays with matching dimensions."))); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + /* reset the current array dimension size counter */ ctx->sizes[ndim] = 0; @@ -2573,7 +2588,10 @@ populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv) ctx->aio->element_type, ctx->aio->element_typmod, NULL, ctx->mcxt, PointerGetDatum(NULL), - jsv, &element_isnull); + jsv, &element_isnull, ctx->escontext); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; accumArrayResult(ctx->astate, element, element_isnull, ctx->aio->element_type, ctx->acxt); @@ -2594,6 +2612,10 @@ populate_array_object_start(void *_state) else if (ndim < state->ctx->ndims) populate_array_report_expected_array(state->ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2611,6 +2633,10 @@ populate_array_array_end(void *_state) if (ndim < ctx->ndims) populate_array_check_dimension(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(state->ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + return JSON_SUCCESS; } @@ -2686,6 +2712,10 @@ populate_array_scalar(void *_state, char *token, JsonTokenType tokentype) else if (ndim < ctx->ndims) populate_array_report_expected_array(ctx, ndim); + /* Report that an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return JSON_SEM_ACTION_FAILED; + if (ndim == ctx->ndims) { /* remember the scalar element token */ @@ -2715,7 +2745,13 @@ populate_array_json(PopulateArrayContext *ctx, char *json, int len) sem.array_element_end = populate_array_element_end; sem.scalar = populate_array_scalar; - pg_parse_json_or_ereport(state.lex, &sem); + if (!pg_parse_json_or_errsave(state.lex, &sem, ctx->escontext)) + { + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + pfree(state.lex); + return; + } /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2740,10 +2776,15 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ check_stack_depth(); - if (jbv->type != jbvBinary || !JsonContainerIsArray(jbc)) + if (jbv->type != jbvBinary || + !JsonContainerIsArray(jbc) || + JsonContainerIsScalar(jbc)) + { populate_array_report_expected_array(ctx, ndim - 1); - - Assert(!JsonContainerIsScalar(jbc)); + /* Nothing to do on an error. */ + Assert(SOFT_ERROR_OCCURRED(ctx->escontext)); + return; + } it = JsonbIteratorInit(jbc); @@ -2762,7 +2803,12 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ (tok == WJB_ELEM && (val.type != jbvBinary || !JsonContainerIsArray(val.val.binary.data))))) + { populate_array_assign_ndims(ctx, ndim); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; + } jsv.is_json = false; jsv.val.jsonb = &val; @@ -2780,6 +2826,9 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ { /* populate child sub-array */ populate_array_dim_jsonb(ctx, &val, ndim + 1); + /* Nothing to do on an error. */ + if (SOFT_ERROR_OCCURRED(ctx->escontext)) + return; /* number of dimensions should be already known */ Assert(ctx->ndims > 0 && ctx->dims); @@ -2797,12 +2846,18 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */ Assert(tok == WJB_DONE && !it); } -/* recursively populate an array from json/jsonb */ +/* + * Recursively populate an array from json/jsonb + * + * *isnull is set to true if an error is reported during parsing. + */ static Datum populate_array(ArrayIOData *aio, const char *colname, MemoryContext mcxt, - JsValue *jsv) + JsValue *jsv, + Node *escontext, + bool *isnull) { PopulateArrayContext ctx; Datum result; @@ -2817,6 +2872,7 @@ populate_array(ArrayIOData *aio, ctx.ndims = 0; /* unknown yet */ ctx.dims = NULL; ctx.sizes = NULL; + ctx.escontext = escontext; if (jsv->is_json) populate_array_json(&ctx, jsv->val.json.str, @@ -2825,7 +2881,16 @@ populate_array(ArrayIOData *aio, else { populate_array_dim_jsonb(&ctx, jsv->val.jsonb, 1); - ctx.dims[0] = ctx.sizes[0]; + /* Nothing to do on an error. */ + if (!SOFT_ERROR_OCCURRED(ctx.escontext)) + ctx.dims[0] = ctx.sizes[0]; + } + + /* Nothing to return if an error occurred. */ + if (SOFT_ERROR_OCCURRED(ctx.escontext)) + { + *isnull = true; + return (Datum) 0; } Assert(ctx.ndims > 0); @@ -2842,6 +2907,7 @@ populate_array(ArrayIOData *aio, pfree(ctx.sizes); pfree(lbs); + *isnull = false; return result; } @@ -2957,7 +3023,8 @@ populate_composite(CompositeIOData *io, /* populate non-null scalar value from json/jsonb value */ static Datum -populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) +populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv, + Node *escontext) { Datum res; char *str = NULL; @@ -3028,7 +3095,11 @@ populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv) elog(ERROR, "unrecognized jsonb type: %d", (int) jbv->type); } - res = InputFunctionCall(&io->typiofunc, str, io->typioparam, typmod); + if (!InputFunctionCallSafe(&io->typiofunc, str, io->typioparam, typmod, + escontext, &res)) + { + res = (Datum) 0; + } /* free temporary buffer */ if (str != json) @@ -3054,7 +3125,7 @@ populate_domain(DomainIOData *io, res = populate_record_field(io->base_io, io->base_typid, io->base_typmod, colname, mcxt, PointerGetDatum(NULL), - jsv, &isnull); + jsv, &isnull, NULL); Assert(!isnull); } @@ -3159,7 +3230,8 @@ populate_record_field(ColumnIOData *col, MemoryContext mcxt, Datum defaultval, JsValue *jsv, - bool *isnull) + bool *isnull, + Node *escontext) { TypeCat typcat; @@ -3192,10 +3264,12 @@ populate_record_field(ColumnIOData *col, switch (typcat) { case TYPECAT_SCALAR: - return populate_scalar(&col->scalar_io, typid, typmod, jsv); + return populate_scalar(&col->scalar_io, typid, typmod, jsv, + escontext); case TYPECAT_ARRAY: - return populate_array(&col->io.array, colname, mcxt, jsv); + return populate_array(&col->io.array, colname, mcxt, jsv, + escontext, isnull); case TYPECAT_COMPOSITE: case TYPECAT_COMPOSITE_DOMAIN: @@ -3216,6 +3290,53 @@ populate_record_field(ColumnIOData *col, } } +/* recursively populate specified type from a json/jsonb value */ +Datum +json_populate_type(Datum json_val, Oid json_type, Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext) +{ + JsValue jsv = {0}; + JsonbValue jbv; + + jsv.is_json = json_type == JSONOID; + + if (*isnull) + { + if (jsv.is_json) + jsv.val.json.str = NULL; + else + jsv.val.jsonb = NULL; + } + else if (jsv.is_json) + { + text *json = DatumGetTextPP(json_val); + + jsv.val.json.str = VARDATA_ANY(json); + jsv.val.json.len = VARSIZE_ANY_EXHDR(json); + jsv.val.json.type = JSON_TOKEN_INVALID; /* not used in + * populate_composite() */ + } + else + { + Jsonb *jsonb = DatumGetJsonbP(json_val); + + jsv.val.jsonb = &jbv; + + /* fill binary jsonb value pointing to jb */ + jbv.type = jbvBinary; + jbv.val.binary.data = &jsonb->root; + jbv.val.binary.len = VARSIZE(jsonb) - VARHDRSZ; + } + + if (!*cache) + *cache = MemoryContextAllocZero(mcxt, sizeof(ColumnIOData)); + + return populate_record_field(*cache, typid, typmod, NULL, mcxt, + PointerGetDatum(NULL), &jsv, isnull, + escontext); +} + static RecordIOData * allocate_record_info(MemoryContext mcxt, int ncolumns) { @@ -3357,7 +3478,8 @@ populate_record(TupleDesc tupdesc, mcxt, nulls[i] ? (Datum) 0 : values[i], &field, - &nulls[i]); + &nulls[i], + NULL); } res = heap_form_tuple(tupdesc, values, nulls); diff --git a/src/backend/utils/adt/jsonpath.c b/src/backend/utils/adt/jsonpath.c index c5ba3b7f1d..8d89994c10 100644 --- a/src/backend/utils/adt/jsonpath.c +++ b/src/backend/utils/adt/jsonpath.c @@ -68,7 +68,9 @@ #include "libpq/pqformat.h" #include "nodes/miscnodes.h" #include "miscadmin.h" +#include "nodes/nodeFuncs.h" #include "utils/builtins.h" +#include "utils/formatting.h" #include "utils/json.h" #include "utils/jsonpath.h" @@ -1110,3 +1112,256 @@ jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, return true; } + +/* SQL/JSON datatype status: */ +typedef enum JsonPathDatatypeStatus +{ + jpdsNonDateTime, /* null, bool, numeric, string, array, object */ + jpdsUnknownDateTime, /* unknown datetime type */ + jpdsDateTimeZoned, /* timetz, timestamptz */ + jpdsDateTimeNonZoned /* time, timestamp, date */ +} JsonPathDatatypeStatus; + +/* Context for jspIsMutableWalker() */ +typedef struct JsonPathMutableContext +{ + List *varnames; /* list of variable names */ + List *varexprs; /* list of variable expressions */ + JsonPathDatatypeStatus current; /* status of @ item */ + bool lax; /* jsonpath is lax or strict */ + bool mutable; /* resulting mutability status */ +} JsonPathMutableContext; + +/* + * Recursive walker for jspIsMutable() + */ +static JsonPathDatatypeStatus +jspIsMutableWalker(JsonPathItem *jpi, JsonPathMutableContext *cxt) +{ + JsonPathItem next; + JsonPathDatatypeStatus status = jpdsNonDateTime; + + while (!cxt->mutable) + { + JsonPathItem arg; + JsonPathDatatypeStatus leftStatus; + JsonPathDatatypeStatus rightStatus; + + switch (jpi->type) + { + case jpiRoot: + Assert(status == jpdsNonDateTime); + break; + + case jpiCurrent: + Assert(status == jpdsNonDateTime); + status = cxt->current; + break; + + case jpiFilter: + { + JsonPathDatatypeStatus prevStatus = cxt->current; + + cxt->current = status; + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + + cxt->current = prevStatus; + break; + } + + case jpiVariable: + { + int32 len; + const char *name = jspGetString(jpi, &len); + ListCell *lc1; + ListCell *lc2; + + Assert(status == jpdsNonDateTime); + + forboth(lc1, cxt->varnames, lc2, cxt->varexprs) + { + String *varname = lfirst_node(String, lc1); + Node *varexpr = lfirst(lc2); + + if (strncmp(varname->sval, name, len)) + continue; + + switch (exprType(varexpr)) + { + case DATEOID: + case TIMEOID: + case TIMESTAMPOID: + status = jpdsDateTimeNonZoned; + break; + + case TIMETZOID: + case TIMESTAMPTZOID: + status = jpdsDateTimeZoned; + break; + + default: + status = jpdsNonDateTime; + break; + } + + break; + } + break; + } + + case jpiEqual: + case jpiNotEqual: + case jpiLess: + case jpiGreater: + case jpiLessOrEqual: + case jpiGreaterOrEqual: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + leftStatus = jspIsMutableWalker(&arg, cxt); + + jspGetRightArg(jpi, &arg); + rightStatus = jspIsMutableWalker(&arg, cxt); + + /* + * Comparison of datetime type with different timezone status + * is mutable. + */ + if (leftStatus != jpdsNonDateTime && + rightStatus != jpdsNonDateTime && + (leftStatus == jpdsUnknownDateTime || + rightStatus == jpdsUnknownDateTime || + leftStatus != rightStatus)) + cxt->mutable = true; + break; + + case jpiNot: + case jpiIsUnknown: + case jpiExists: + case jpiPlus: + case jpiMinus: + Assert(status == jpdsNonDateTime); + jspGetArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiAnd: + case jpiOr: + case jpiAdd: + case jpiSub: + case jpiMul: + case jpiDiv: + case jpiMod: + case jpiStartsWith: + Assert(status == jpdsNonDateTime); + jspGetLeftArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + jspGetRightArg(jpi, &arg); + jspIsMutableWalker(&arg, cxt); + break; + + case jpiIndexArray: + for (int i = 0; i < jpi->content.array.nelems; i++) + { + JsonPathItem from; + JsonPathItem to; + + if (jspGetArraySubscript(jpi, &from, &to, i)) + jspIsMutableWalker(&to, cxt); + + jspIsMutableWalker(&from, cxt); + } + /* FALLTHROUGH */ + + case jpiAnyArray: + if (!cxt->lax) + status = jpdsNonDateTime; + break; + + case jpiAny: + if (jpi->content.anybounds.first > 0) + status = jpdsNonDateTime; + break; + + case jpiDatetime: + if (jpi->content.arg) + { + char *template; + + jspGetArg(jpi, &arg); + if (arg.type != jpiString) + { + status = jpdsNonDateTime; + break; /* there will be runtime error */ + } + + template = jspGetString(&arg, NULL); + if (datetime_format_has_tz(template)) + status = jpdsDateTimeZoned; + else + status = jpdsDateTimeNonZoned; + } + else + { + status = jpdsUnknownDateTime; + } + break; + + case jpiLikeRegex: + Assert(status == jpdsNonDateTime); + jspInitByBuffer(&arg, jpi->base, jpi->content.like_regex.expr); + jspIsMutableWalker(&arg, cxt); + break; + + /* literals */ + case jpiNull: + case jpiString: + case jpiNumeric: + case jpiBool: + /* accessors */ + case jpiKey: + case jpiAnyKey: + /* special items */ + case jpiSubscript: + case jpiLast: + /* item methods */ + case jpiType: + case jpiSize: + case jpiAbs: + case jpiFloor: + case jpiCeiling: + case jpiDouble: + case jpiKeyValue: + status = jpdsNonDateTime; + break; + } + + if (!jspGetNext(jpi, &next)) + break; + + jpi = &next; + } + + return status; +} + +/* + * Check whether jsonpath expression is immutable or not. + */ +bool +jspIsMutable(JsonPath *path, List *varnames, List *varexprs) +{ + JsonPathMutableContext cxt; + JsonPathItem jpi; + + cxt.varnames = varnames; + cxt.varexprs = varexprs; + cxt.current = jpdsNonDateTime; + cxt.lax = (path->header & JSONPATH_LAX) != 0; + cxt.mutable = false; + + jspInit(&jpi, path); + jspIsMutableWalker(&jpi, &cxt); + + return cxt.mutable; +} diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index 2d0599b4aa..bf4a7220fd 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -87,12 +87,16 @@ typedef struct JsonBaseObjectInfo int id; } JsonBaseObjectInfo; +typedef int (*JsonPathVarCallback) (void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); + /* * Context of jsonpath execution. */ typedef struct JsonPathExecContext { - Jsonb *vars; /* variables to substitute into jsonpath */ + void *vars; /* variables to substitute into jsonpath */ + JsonPathVarCallback getVar; JsonbValue *root; /* for $ evaluation */ JsonbValue *current; /* for @ evaluation */ JsonBaseObjectInfo baseObject; /* "base object" for .keyvalue() @@ -174,7 +178,8 @@ typedef JsonPathBool (*JsonPathPredicateCallback) (JsonPathItem *jsp, void *param); typedef Numeric (*BinaryArithmFunc) (Numeric num1, Numeric num2, bool *error); -static JsonPathExecResult executeJsonPath(JsonPath *path, Jsonb *vars, +static JsonPathExecResult executeJsonPath(JsonPath *path, void *vars, + JsonPathVarCallback getVar, Jsonb *json, bool throwErrors, JsonValueList *result, bool useTz); static JsonPathExecResult executeItem(JsonPathExecContext *cxt, @@ -225,8 +230,13 @@ static JsonPathExecResult appendBoolResult(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonValueList *found, JsonPathBool res); static void getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, JsonbValue *value); +static int GetJsonPathVar(void *vars, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject); static void getJsonPathVariable(JsonPathExecContext *cxt, - JsonPathItem *variable, Jsonb *vars, JsonbValue *value); + JsonPathItem *variable, JsonbValue *value); +static int getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, + int varNameLen, JsonbValue *val, + JsonbValue *baseObject); static int JsonbArraySize(JsonbValue *jb); static JsonPathBool executeComparison(JsonPathItem *cmp, JsonbValue *lv, JsonbValue *rv, void *p); @@ -284,7 +294,8 @@ jsonb_path_exists_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - res = executeJsonPath(jp, vars, jb, !silent, NULL, tz); + res = executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, NULL, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -339,7 +350,8 @@ jsonb_path_match_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); @@ -417,7 +429,8 @@ jsonb_path_query_internal(FunctionCallInfo fcinfo, bool tz) vars = PG_GETARG_JSONB_P_COPY(2); silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); funcctx->user_fctx = JsonValueListGetList(&found); @@ -464,7 +477,8 @@ jsonb_path_query_array_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); PG_RETURN_JSONB_P(JsonbValueToJsonb(wrapItemsInArray(&found))); } @@ -495,7 +509,8 @@ jsonb_path_query_first_internal(FunctionCallInfo fcinfo, bool tz) Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); - (void) executeJsonPath(jp, vars, jb, !silent, &found, tz); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, + jb, !silent, &found, tz); if (JsonValueListLength(&found) >= 1) PG_RETURN_JSONB_P(JsonbValueToJsonb(JsonValueListHead(&found))); @@ -537,8 +552,9 @@ jsonb_path_query_first_tz(PG_FUNCTION_ARGS) * In other case it tries to find all the satisfied result items. */ static JsonPathExecResult -executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, - JsonValueList *result, bool useTz) +executeJsonPath(JsonPath *path, void *vars, JsonPathVarCallback getVar, + Jsonb *json, bool throwErrors, JsonValueList *result, + bool useTz) { JsonPathExecContext cxt; JsonPathExecResult res; @@ -550,22 +566,16 @@ executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, if (!JsonbExtractScalar(&json->root, &jbv)) JsonbInitBinary(&jbv, json); - if (vars && !JsonContainerIsObject(&vars->root)) - { - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"vars\" argument is not an object"), - errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); - } - cxt.vars = vars; + cxt.getVar = getVar; cxt.laxMode = (path->header & JSONPATH_LAX) != 0; cxt.ignoreStructuralErrors = cxt.laxMode; cxt.root = &jbv; cxt.current = &jbv; cxt.baseObject.jbc = NULL; cxt.baseObject.id = 0; - cxt.lastGeneratedObjectId = vars ? 2 : 1; + /* 1 + number of base objects in vars */ + cxt.lastGeneratedObjectId = 1 + getVar(vars, NULL, 0, NULL, NULL); cxt.innermostArraySize = -1; cxt.throwErrors = throwErrors; cxt.useTz = useTz; @@ -2108,54 +2118,118 @@ getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item, &value->val.string.len); break; case jpiVariable: - getJsonPathVariable(cxt, item, cxt->vars, value); + getJsonPathVariable(cxt, item, value); return; default: elog(ERROR, "unexpected jsonpath item type"); } } +/* + * Returns the computed value of a JSON path variable with given name. + */ +static int +GetJsonPathVar(void *cxt, char *varName, int varNameLen, + JsonbValue *val, JsonbValue *baseObject) +{ + JsonPathVariable *var = NULL; + List *vars = cxt; + ListCell *lc; + int id = 1; + + if (!varName) + return list_length(vars); + + foreach(lc, vars) + { + JsonPathVariable *curvar = lfirst(lc); + + if (!strncmp(curvar->name, varName, varNameLen)) + { + var = curvar; + break; + } + + id++; + } + + if (!var) + return -1; + + if (var->isnull) + { + val->type = jbvNull; + return 0; + } + + JsonItemFromDatum(var->value, var->typid, var->typmod, val); + + *baseObject = *val; + return id; +} + /* * Get the value of variable passed to jsonpath executor */ static void getJsonPathVariable(JsonPathExecContext *cxt, JsonPathItem *variable, - Jsonb *vars, JsonbValue *value) + JsonbValue *value) { char *varName; int varNameLength; + JsonbValue baseObject; + int baseObjectId; + + Assert(variable->type == jpiVariable); + varName = jspGetString(variable, &varNameLength); + + if (!cxt->vars || + (baseObjectId = cxt->getVar(cxt->vars, varName, varNameLength, value, + &baseObject)) < 0) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("could not find jsonpath variable \"%s\"", + pnstrdup(varName, varNameLength)))); + + if (baseObjectId > 0) + setBaseObject(cxt, &baseObject, baseObjectId); +} + +static int +getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, int varNameLength, + JsonbValue *value, JsonbValue *baseObject) +{ + Jsonb *vars = varsJsonb; JsonbValue tmp; JsonbValue *v; - if (!vars) + if (!varName) { - value->type = jbvNull; - return; + if (vars && !JsonContainerIsObject(&vars->root)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"vars\" argument is not an object"), + errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object."))); + } + + return vars ? 1 : 0; /* count of base objects */ } - Assert(variable->type == jpiVariable); - varName = jspGetString(variable, &varNameLength); tmp.type = jbvString; tmp.val.string.val = varName; tmp.val.string.len = varNameLength; v = findJsonbValueFromContainer(&vars->root, JB_FOBJECT, &tmp); - if (v) - { - *value = *v; - pfree(v); - } - else - { - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("could not find jsonpath variable \"%s\"", - pnstrdup(varName, varNameLength)))); - } + if (!v) + return -1; - JsonbInitBinary(&tmp, vars); - setBaseObject(cxt, &tmp, 1); + *value = *v; + pfree(v); + + JsonbInitBinary(baseObject, vars); + return 1; } /**************** Support functions for JsonPath execution *****************/ @@ -2812,3 +2886,240 @@ compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, return DatumGetInt32(DirectFunctionCall2(cmpfunc, val1, val2)); } + +/* Executor-callable JSON_EXISTS implementation */ +bool +JsonPathExists(Datum jb, JsonPath *jp, bool *error, List *vars) +{ + JsonPathExecResult res = executeJsonPath(jp, vars, GetJsonPathVar, + DatumGetJsonbP(jb), !error, NULL, + true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + *error = true; + + return res == jperOk; +} + +/* Executor-callable JSON_QUERY implementation */ +Datum +JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, bool *empty, + bool *error, List *vars) +{ + JsonbValue *first; + bool wrap; + JsonValueList found = {0}; + JsonPathExecResult res PG_USED_FOR_ASSERTS_ONLY; + int count; + + res = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(res)); + + if (error && jperIsError(res)) + { + *error = true; + *empty = false; + return (Datum) 0; + } + + count = JsonValueListLength(&found); + + first = count ? JsonValueListHead(&found) : NULL; + + if (!first) + wrap = false; + else if (wrapper == JSW_NONE) + wrap = false; + else if (wrapper == JSW_UNCONDITIONAL) + wrap = true; + else if (wrapper == JSW_CONDITIONAL) + wrap = count > 1 || + IsAJsonbScalar(first) || + (first->type == jbvBinary && + JsonContainerIsScalar(first->val.binary.data)); + else + { + elog(ERROR, "unrecognized json wrapper %d", wrapper); + wrap = false; + } + + if (wrap) + return JsonbPGetDatum(JsonbValueToJsonb(wrapItemsInArray(&found))); + + if (count > 1) + { + if (error) + { + *error = true; + return (Datum) 0; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_QUERY should return singleton item without wrapper"), + errhint("Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array."))); + } + + if (first) + return JsonbPGetDatum(JsonbValueToJsonb(first)); + + *empty = true; + return PointerGetDatum(NULL); +} + +/* Executor-callable JSON_VALUE implementation */ +JsonbValue * +JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars) +{ + JsonbValue *res; + JsonValueList found = {0}; + JsonPathExecResult jper PG_USED_FOR_ASSERTS_ONLY; + int count; + + jper = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb), + !error, &found, true); + + Assert(error || !jperIsError(jper)); + + if (error && jperIsError(jper)) + { + *error = true; + *empty = false; + return NULL; + } + + count = JsonValueListLength(&found); + + *empty = !count; + + if (*empty) + return NULL; + + if (count > 1) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + res = JsonValueListHead(&found); + + if (res->type == jbvBinary && + JsonContainerIsScalar(res->val.binary.data)) + JsonbExtractScalar(res->val.binary.data, res); + + if (!IsAJsonbScalar(res)) + { + if (error) + { + *error = true; + return NULL; + } + + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_SCALAR_REQUIRED), + errmsg("JSON path expression in JSON_VALUE should return singleton scalar item"))); + } + + if (res->type == jbvNull) + return NULL; + + return res; +} + +static void +JsonbValueInitNumericDatum(JsonbValue *jbv, Datum num) +{ + jbv->type = jbvNumeric; + jbv->val.numeric = DatumGetNumeric(num); +} + +void +JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) +{ + switch (typid) + { + case BOOLOID: + res->type = jbvBool; + res->val.boolean = DatumGetBool(val); + break; + case NUMERICOID: + JsonbValueInitNumericDatum(res, val); + break; + case INT2OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int2_numeric, val)); + break; + case INT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int4_numeric, val)); + break; + case INT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(int8_numeric, val)); + break; + case FLOAT4OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float4_numeric, val)); + break; + case FLOAT8OID: + JsonbValueInitNumericDatum(res, DirectFunctionCall1(float8_numeric, val)); + break; + case TEXTOID: + case VARCHAROID: + res->type = jbvString; + res->val.string.val = VARDATA_ANY(val); + res->val.string.len = VARSIZE_ANY_EXHDR(val); + break; + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + res->type = jbvDatetime; + res->val.datetime.value = val; + res->val.datetime.typid = typid; + res->val.datetime.typmod = typmod; + res->val.datetime.tz = 0; + break; + case JSONBOID: + { + JsonbValue *jbv = res; + Jsonb *jb = DatumGetJsonbP(val); + + if (JsonContainerIsScalar(&jb->root)) + { + bool result PG_USED_FOR_ASSERTS_ONLY; + + result = JsonbExtractScalar(&jb->root, jbv); + Assert(result); + } + else + JsonbInitBinary(jbv, jb); + break; + } + case JSONOID: + { + text *txt = DatumGetTextP(val); + char *str = text_to_cstring(txt); + Jsonb *jb; + + jb = DatumGetJsonbP(DirectFunctionCall1(jsonb_in, + CStringGetDatum(str))); + pfree(str); + + JsonItemFromDatum(JsonbPGetDatum(jb), JSONBOID, -1, res); + break; + } + default: + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types"))); + } +} diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 03f2835c3f..62b23964c8 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -476,6 +476,8 @@ static void get_const_expr(Const *constval, deparse_context *context, int showtype); static void get_const_collation(Const *constval, deparse_context *context); static void get_json_format(JsonFormat *format, StringInfo buf); +static void get_json_returning(JsonReturning *returning, StringInfo buf, + bool json_format_by_default); static void get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, bool showimplicit); static void get_json_constructor_options(JsonConstructorExpr *ctor, @@ -518,6 +520,8 @@ static char *generate_qualified_type_name(Oid typid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); +static void get_json_path_spec(Node *path_spec, deparse_context *context, + bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8279,6 +8283,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_WindowFunc: case T_FuncExpr: case T_JsonConstructorExpr: + case T_JsonExpr: /* function-like: name(..) or name[..] */ return true; @@ -8450,6 +8455,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) case T_GroupingFunc: /* own parentheses */ case T_WindowFunc: /* own parentheses */ case T_CaseExpr: /* other separators */ + case T_JsonExpr: /* own parentheses */ return true; default: return false; @@ -8565,6 +8571,65 @@ get_rule_expr_paren(Node *node, deparse_context *context, appendStringInfoChar(context->buf, ')'); } +static void +get_json_behavior(JsonBehavior *behavior, deparse_context *context, + const char *on) +{ + /* + * The order of array elements must correspond to the order of + * JsonBehaviorType members. + */ + const char *behavior_names[] = + { + " NULL", + " ERROR", + " EMPTY", + " TRUE", + " FALSE", + " UNKNOWN", + " EMPTY ARRAY", + " EMPTY OBJECT", + " DEFAULT " + }; + + if ((int) behavior->btype < 0 || behavior->btype >= lengthof(behavior_names)) + elog(ERROR, "invalid json behavior type: %d", behavior->btype); + + appendStringInfoString(context->buf, behavior_names[behavior->btype]); + + if (behavior->btype == JSON_BEHAVIOR_DEFAULT) + get_rule_expr(behavior->default_expr, context, false); + + appendStringInfo(context->buf, " ON %s", on); +} + +/* + * get_json_expr_options + * + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + */ +static void +get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, + JsonBehaviorType default_behavior) +{ + if (jsexpr->op == JSON_QUERY_OP) + { + if (jsexpr->wrapper == JSW_CONDITIONAL) + appendStringInfo(context->buf, " WITH CONDITIONAL WRAPPER"); + else if (jsexpr->wrapper == JSW_UNCONDITIONAL) + appendStringInfo(context->buf, " WITH UNCONDITIONAL WRAPPER"); + + if (jsexpr->omit_quotes) + appendStringInfo(context->buf, " OMIT QUOTES"); + } + + if (jsexpr->op != JSON_EXISTS_OP && + jsexpr->on_empty->btype != default_behavior) + get_json_behavior(jsexpr->on_empty, context, "EMPTY"); + + if (jsexpr->on_error->btype != default_behavior) + get_json_behavior(jsexpr->on_error, context, "ERROR"); +} /* ---------- * get_rule_expr - Parse back an expression @@ -9724,6 +9789,7 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonValueExpr: { JsonValueExpr *jve = (JsonValueExpr *) node; @@ -9773,6 +9839,64 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_JsonExpr: + { + JsonExpr *jexpr = (JsonExpr *) node; + + switch (jexpr->op) + { + case JSON_EXISTS_OP: + appendStringInfoString(buf, "JSON_EXISTS("); + break; + case JSON_QUERY_OP: + appendStringInfoString(buf, "JSON_QUERY("); + break; + case JSON_VALUE_OP: + appendStringInfoString(buf, "JSON_VALUE("); + break; + } + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_json_path_spec(jexpr->path_spec, context, showimplicit); + + if (jexpr->passing_values) + { + ListCell *lc1, + *lc2; + bool needcomma = false; + + appendStringInfoString(buf, " PASSING "); + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + get_rule_expr((Node *) lfirst(lc2), context, showimplicit); + appendStringInfo(buf, " AS %s", + ((String *) lfirst_node(String, lc1))->sval); + } + } + + if (jexpr->op != JSON_EXISTS_OP || + jexpr->returning->typid != BOOLOID) + get_json_returning(jexpr->returning, context->buf, + jexpr->op == JSON_QUERY_OP); + + get_json_expr_options(jexpr, context, + jexpr->op != JSON_EXISTS_OP ? + JSON_BEHAVIOR_NULL : + JSON_BEHAVIOR_FALSE); + + appendStringInfoString(buf, ")"); + } + break; + case T_List: { char *sep; @@ -9896,6 +10020,7 @@ looks_like_function(Node *node) case T_MinMaxExpr: case T_SQLValueFunction: case T_XmlExpr: + case T_JsonExpr: /* these are all accepted by func_expr_common_subexpr */ return true; default: @@ -10755,6 +10880,18 @@ get_const_collation(Const *constval, deparse_context *context) } } +/* + * get_json_path_spec - Parse back a JSON path specification + */ +static void +get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit) +{ + if (IsA(path_spec, Const)) + get_const_expr((Const *) path_spec, context, -1); + else + get_rule_expr(path_spec, context, showimplicit); +} + /* * get_json_format - Parse back a JsonFormat node */ diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index 048573c2bc..9ab0237c79 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -16,12 +16,15 @@ #include "executor/nodeAgg.h" #include "nodes/execnodes.h" +#include "nodes/miscnodes.h" /* forward references to avoid circularity */ struct ExprEvalStep; struct SubscriptingRefState; struct ScalarArrayOpExprHashTable; struct JsonConstructorExprState; +struct JsonbValue; +struct JsonExprState; /* Bits in ExprState->flags (see also execnodes.h for public flag bits): */ /* expression's interpreter has been initialized */ @@ -238,6 +241,11 @@ typedef enum ExprEvalOp EEOP_XMLEXPR, EEOP_JSON_CONSTRUCTOR, EEOP_IS_JSON, + EEOP_JSONEXPR_SKIP, + EEOP_JSONEXPR_PATH, + EEOP_JSONEXPR_BEHAVIOR, + EEOP_JSONEXPR_COERCION, + EEOP_JSONEXPR_COERCION_FINISH, EEOP_AGGREF, EEOP_GROUPING_FUNC, EEOP_WINDOW_FUNC, @@ -689,6 +697,57 @@ typedef struct ExprEvalStep JsonIsPredicate *pred; /* original expression node */ } is_json; + /* for EEOP_JSONEXPR_PATH */ + struct + { + struct JsonExprState *jsestate; + } jsonexpr; + + /* for EEOP_JSONEXPR_SKIP */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprSkip() */ + int jump_coercion; + int jump_passing_args; + } jsonexpr_skip; + + /* for EEOP_JSONEXPR_BEHAVIOR */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprBehavior() */ + int jump_onerror_expr; + int jump_onempty_expr; + int jump_coercion; + int jump_skip_coercion; + } jsonexpr_behavior; + + /* for EEOP_JSONEXPR_COERCION */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion; + + /* for EEOP_JSONEXPR_COERCION_FINISH */ + struct + { + /* Same as jsonexpr.jsestate */ + struct JsonExprState *jsestate; + + /* See ExecEvalJsonExprCoercion() */ + int jump_coercion_error; + int jump_coercion_done; + } jsonexpr_coercion_finish; } d; } ExprEvalStep; @@ -752,6 +811,105 @@ typedef struct JsonConstructorExprState int nargs; } JsonConstructorExprState; +/* + * Information computed before evaluating a JsonExpr expression. + */ +typedef struct JsonExprPreEvalState +{ + /* value/isnull for JsonExpr.formatted_expr */ + NullableDatum formatted_expr; + + /* value/isnull for JsonExpr.pathspec */ + NullableDatum pathspec; + + /* JsonPathVariable entries for JsonExpr.passing_values */ + List *args; +} JsonExprPreEvalState; + +/* + * State for a given member of JsonItemCoercions. + */ +typedef struct JsonCoercionState +{ + /* Expression used to evaluate the coercion */ + JsonCoercion *coercion; + + /* ExprEvalStep to compute this coercion's expression */ + int jump_eval_expr; +} JsonCoercionState; + +/* + * State for evaluating the coercion for a given JSON item using one of + * the following coercions. + * + * Note that while ExecInitExprRec() for JsonItemCoercions will initialize + * ExprEvalSteps for all of the members that need it, only one will get run + * during a given evaluation of the enclosing JsonExpr depending on the type + * of the result JSON item. + */ +typedef struct JsonItemCoercionsState +{ + JsonCoercionState *null; + JsonCoercionState *string; + JsonCoercionState *numeric; + JsonCoercionState *boolean; + JsonCoercionState *date; + JsonCoercionState *time; + JsonCoercionState *timetz; + JsonCoercionState *timestamp; + JsonCoercionState *timestamptz; + JsonCoercionState *composite; +} JsonItemCoercionsState; + +/* + * State for some post-JsonExpr-evaluation processing steps that gets filled + * in JsonExpr evaluation. + */ +typedef struct JsonExprPostEvalState +{ + /* Is JSON item empty? */ + bool empty; + + /* Did JSON item evaluation cause an error? */ + bool error; + + /* Cache for json_populate_type() called for coercion in some cases */ + void *cache; + + /* + * State for evaluating a JSON item coercion. Points to one of those in + * JsonExprState.coercions chosen by ExecPrepareJsonItemCoercion(). + */ + JsonCoercionState *item_jcstate; + bool coercing_behavior_expr; + bool coercion_error; + bool coercion_done; + + ErrorSaveContext escontext; +} JsonExprPostEvalState; + +/* EEOP_JSONEXPR state, too big to inline */ +typedef struct JsonExprState +{ + /* original expression node */ + JsonExpr *jsexpr; + + JsonExprPreEvalState pre_eval; + JsonExprPostEvalState post_eval; + + struct + { + FmgrInfo *finfo; /* typinput function for output type */ + Oid typioparam; + } input; /* I/O info for output type */ + + /* + * Either of the following two is used by ExecEvalJsonExprCoercion() to + * apply coercion to the final result if needed. + */ + JsonCoercionState *result_jcstate; + JsonItemCoercionsState *item_jcstates; +} JsonExprState; /* functions in execExpr.c */ extern void ExprEvalPushStep(ExprState *es, const ExprEvalStep *s); @@ -805,6 +963,14 @@ extern void ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op); extern void ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op); +extern int ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, + ExprContext *econtext, + Datum res, bool resnull); +extern int ExecEvalJsonExprCoercionFinish(ExprState *state, ExprEvalStep *op); +extern void ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, + ExprContext *econtext); extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op); extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op, ExprContext *econtext); diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index c677e490d7..709ad967ba 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -17,6 +17,7 @@ #include "executor/execdesc.h" #include "fmgr.h" #include "nodes/lockoptions.h" +#include "nodes/miscnodes.h" #include "nodes/parsenodes.h" #include "utils/memutils.h" diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index cb714f4a19..584bf7001a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -129,6 +129,9 @@ typedef struct ExprState Datum *innermost_domainval; bool *innermost_domainnull; + + /* ErrorSaveContext for soft-error capture. */ + Node *escontext; } ExprState; diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 3180703005..c1d437b44e 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -112,6 +112,7 @@ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding, int location); extern JsonValueExpr *makeJsonValueExpr(Expr *raw_expr, Expr *formatted_expr, JsonFormat *format); +extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr); extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index fe003ded50..4e515548d2 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1716,6 +1716,23 @@ typedef struct TriggerTransition /* Nodes for SQL/JSON support */ +/* + * JsonQuotes - + * representation of [KEEP|OMIT] QUOTES clause for JSON_QUERY() + */ +typedef enum JsonQuotes +{ + JS_QUOTES_UNSPEC, /* unspecified */ + JS_QUOTES_KEEP, /* KEEP QUOTES */ + JS_QUOTES_OMIT /* OMIT QUOTES */ +} JsonQuotes; + +/* + * JsonPathSpec - + * representation of JSON path constant + */ +typedef char *JsonPathSpec; + /* * JsonOutput - * representation of JSON output clause (RETURNING type [FORMAT format]) @@ -1727,6 +1744,48 @@ typedef struct JsonOutput JsonReturning *returning; /* RETURNING FORMAT clause and type Oids */ } JsonOutput; +/* + * JsonArgument - + * representation of argument from JSON PASSING clause + */ +typedef struct JsonArgument +{ + NodeTag type; + JsonValueExpr *val; /* argument value expression */ + char *name; /* argument name */ +} JsonArgument; + +/* + * JsonCommon - + * representation of common syntax of functions using JSON path + */ +typedef struct JsonCommon +{ + NodeTag type; + JsonValueExpr *expr; /* context item expression */ + Node *pathspec; /* JSON path specification expression */ + char *pathname; /* path name, if any */ + List *passing; /* list of PASSING clause arguments, if any */ + int location; /* token location, or -1 if unknown */ +} JsonCommon; + +/* + * JsonFuncExpr - + * untransformed representation of JSON function expressions + */ +typedef struct JsonFuncExpr +{ + NodeTag type; + JsonExprOp op; /* expression type */ + JsonCommon *common; /* common syntax */ + JsonOutput *output; /* output clause, if specified */ + JsonBehavior *on_empty; /* ON EMPTY behavior, if specified */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + JsonWrapper wrapper; /* array wrapper behavior (JSON_QUERY only) */ + JsonQuotes quotes; /* omit or keep quotes? (JSON_QUERY only) */ + int location; /* token location, or -1 if unknown */ +} JsonFuncExpr; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 60d72a876b..a6283ba2c0 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1544,6 +1544,17 @@ typedef struct XmlExpr int location; } XmlExpr; +/* + * JsonExprOp - + * enumeration of JSON functions using JSON path + */ +typedef enum JsonExprOp +{ + JSON_VALUE_OP, /* JSON_VALUE() */ + JSON_QUERY_OP, /* JSON_QUERY() */ + JSON_EXISTS_OP /* JSON_EXISTS() */ +} JsonExprOp; + /* * JsonEncoding - * representation of JSON ENCODING clause @@ -1568,6 +1579,37 @@ typedef enum JsonFormatType * jsonb */ } JsonFormatType; +/* + * JsonBehaviorType - + * enumeration of behavior types used in JSON ON ... BEHAVIOR clause + * + * If enum members are reordered, get_json_behavior() from ruleutils.c + * must be updated accordingly. + */ +typedef enum JsonBehaviorType +{ + JSON_BEHAVIOR_NULL = 0, + JSON_BEHAVIOR_ERROR, + JSON_BEHAVIOR_EMPTY, + JSON_BEHAVIOR_TRUE, + JSON_BEHAVIOR_FALSE, + JSON_BEHAVIOR_UNKNOWN, + JSON_BEHAVIOR_EMPTY_ARRAY, + JSON_BEHAVIOR_EMPTY_OBJECT, + JSON_BEHAVIOR_DEFAULT +} JsonBehaviorType; + +/* + * JsonWrapper - + * representation of WRAPPER clause for JSON_QUERY() + */ +typedef enum JsonWrapper +{ + JSW_NONE, + JSW_CONDITIONAL, + JSW_UNCONDITIONAL, +} JsonWrapper; + /* * JsonFormat - * representation of JSON FORMAT clause @@ -1662,6 +1704,73 @@ typedef struct JsonIsPredicate int location; /* token location, or -1 if unknown */ } JsonIsPredicate; +/* + * JsonBehavior - + * representation of JSON ON ... BEHAVIOR clause + */ +typedef struct JsonBehavior +{ + NodeTag type; + JsonBehaviorType btype; /* behavior type */ + Node *default_expr; /* default expression, if any */ +} JsonBehavior; + +/* + * JsonCoercion - + * coercion from SQL/JSON item types to SQL types + */ +typedef struct JsonCoercion +{ + NodeTag type; + Node *expr; /* resulting expression coerced to target type */ + bool via_populate; /* coerce result using json_populate_type()? */ + bool via_io; /* coerce result using type input function? */ + Oid collation; /* collation for coercion via I/O or populate */ +} JsonCoercion; + +/* + * JsonItemCoercions - + * expressions for coercion from SQL/JSON item types directly to the + * output SQL type + */ +typedef struct JsonItemCoercions +{ + NodeTag type; + JsonCoercion *null; + JsonCoercion *string; + JsonCoercion *numeric; + JsonCoercion *boolean; + JsonCoercion *date; + JsonCoercion *time; + JsonCoercion *timetz; + JsonCoercion *timestamp; + JsonCoercion *timestamptz; + JsonCoercion *composite; /* arrays and objects */ +} JsonItemCoercions; + +/* + * JsonExpr - + * transformed representation of JSON_VALUE(), JSON_QUERY(), JSON_EXISTS() + */ +typedef struct JsonExpr +{ + Expr xpr; + JsonExprOp op; /* json function ID */ + Node *formatted_expr; /* formatted context item expression */ + JsonCoercion *result_coercion; /* resulting coercion to RETURNING type */ + JsonFormat *format; /* context item format (JSON/JSONB) */ + Node *path_spec; /* JSON path specification expression */ + List *passing_names; /* PASSING argument names */ + List *passing_values; /* PASSING argument values */ + JsonReturning *returning; /* RETURNING clause type/format info */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + JsonItemCoercions *coercions; /* coercions for JSON_VALUE */ + JsonWrapper wrapper; /* WRAPPER for JSON_QUERY */ + bool omit_quotes; /* KEEP/OMIT QUOTES for JSON_QUERY */ + int location; /* token location, or -1 if unknown */ +} JsonExpr; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 5984dcfa4b..0954d9fc7b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -93,6 +93,7 @@ PG_KEYWORD("commit", COMMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("committed", COMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("compression", COMPRESSION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("concurrently", CONCURRENTLY, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("conditional", CONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("configuration", CONFIGURATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("conflict", CONFLICT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("connection", CONNECTION, UNRESERVED_KEYWORD, BARE_LABEL) @@ -147,11 +148,13 @@ PG_KEYWORD("double", DOUBLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("drop", DROP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("each", EACH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("empty", EMPTY_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("event", EVENT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("except", EXCEPT, RESERVED_KEYWORD, AS_LABEL) @@ -233,10 +236,14 @@ PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_exists", JSON_EXISTS, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL) @@ -302,6 +309,7 @@ PG_KEYWORD("off", OFF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("offset", OFFSET, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("oids", OIDS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("old", OLD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("omit", OMIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("on", ON, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("only", ONLY, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("operator", OPERATOR, UNRESERVED_KEYWORD, BARE_LABEL) @@ -344,6 +352,7 @@ PG_KEYWORD("procedures", PROCEDURES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("publication", PUBLICATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("quotes", QUOTES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("range", RANGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("read", READ, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("real", REAL, COL_NAME_KEYWORD, BARE_LABEL) @@ -414,6 +423,7 @@ PG_KEYWORD("stdout", STDOUT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("storage", STORAGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("stored", STORED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("string", STRING_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("subscription", SUBSCRIPTION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("substring", SUBSTRING, COL_NAME_KEYWORD, BARE_LABEL) @@ -449,6 +459,7 @@ PG_KEYWORD("types", TYPES_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uescape", UESCAPE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unbounded", UNBOUNDED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("uncommitted", UNCOMMITTED, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("unconditional", UNCONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("unencrypted", UNENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("union", UNION, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("unique", UNIQUE, RESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/formatting.h b/src/include/utils/formatting.h index 0cad3a2709..d4ca0f42ff 100644 --- a/src/include/utils/formatting.h +++ b/src/include/utils/formatting.h @@ -17,7 +17,6 @@ #ifndef _FORMATTING_H_ #define _FORMATTING_H_ - extern char *str_tolower(const char *buff, size_t nbytes, Oid collid); extern char *str_toupper(const char *buff, size_t nbytes, Oid collid); extern char *str_initcap(const char *buff, size_t nbytes, Oid collid); @@ -29,5 +28,6 @@ extern char *asc_initcap(const char *buff, size_t nbytes); extern Datum parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, Oid *typid, int32 *typmod, int *tz, struct Node *escontext); +extern bool datetime_format_has_tz(const char *fmt_str); #endif diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index 649a1644f2..ac279ee535 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -422,6 +422,9 @@ extern char *JsonbToCString(StringInfo out, JsonbContainer *in, int estimated_len); extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in, int estimated_len); +extern Jsonb *JsonbMakeEmptyArray(void); +extern Jsonb *JsonbMakeEmptyObject(void); +extern char *JsonbUnquote(Jsonb *jb); extern bool JsonbExtractScalar(JsonbContainer *jbc, JsonbValue *res); extern const char *JsonbTypeName(JsonbValue *val); diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index c677ac8ff7..ab543b9423 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -15,6 +15,7 @@ #define JSONFUNCS_H #include "common/jsonapi.h" +#include "nodes/nodes.h" #include "utils/jsonb.h" /* @@ -87,5 +88,9 @@ extern Datum datum_to_json(Datum val, JsonTypeCategory tcategory, extern Datum datum_to_jsonb(Datum val, JsonTypeCategory tcategory, Oid outfuncoid); extern Datum jsonb_from_text(text *js, bool unique_keys); +extern Datum json_populate_type(Datum json_val, Oid json_type, + Oid typid, int32 typmod, + void **cache, MemoryContext mcxt, bool *isnull, + Node *escontext); #endif diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index f0181e045f..5a37133847 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -16,7 +16,9 @@ #include "fmgr.h" #include "nodes/pg_list.h" +#include "nodes/primnodes.h" #include "utils/jsonb.h" +#include "utils/jsonfuncs.h" typedef struct { @@ -184,6 +186,7 @@ extern bool jspGetBool(JsonPathItem *v); extern char *jspGetString(JsonPathItem *v, int32 *len); extern bool jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to, int i); +extern bool jspIsMutable(JsonPath *path, List *varnames, List *varexprs); extern const char *jspOperationName(JsonPathItemType type); @@ -261,4 +264,28 @@ extern bool jspConvertRegexFlags(uint32 xflags, int *result, struct Node *escontext); +/* + * Evaluation of jsonpath + */ + +/* External variable passed into jsonpath. */ +typedef struct JsonPathVariable +{ + char *name; + Oid typid; + int32 typmod; + Datum value; + bool isnull; +} JsonPathVariable; + +/* SQL/JSON item */ +extern void JsonItemFromDatum(Datum val, Oid typid, int32 typmod, + JsonbValue *res); + +extern bool JsonPathExists(Datum jb, JsonPath *path, bool *error, List *vars); +extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, + bool *empty, bool *error, List *vars); +extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, + bool *error, List *vars); + #endif diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer index 435c139ec2..b2aa44f36d 100644 --- a/src/interfaces/ecpg/preproc/ecpg.trailer +++ b/src/interfaces/ecpg/preproc/ecpg.trailer @@ -651,6 +651,34 @@ var_type: simple_type $$.type_index = mm_strdup("-1"); $$.type_sizeof = NULL; } + | STRING_P + { + if (INFORMIX_MODE) + { + /* In Informix mode, "string" is automatically a typedef */ + $$.type_enum = ECPGt_string; + $$.type_str = mm_strdup("char"); + $$.type_dimension = mm_strdup("-1"); + $$.type_index = mm_strdup("-1"); + $$.type_sizeof = NULL; + } + else + { + /* Otherwise, legal only if user typedef'ed it */ + struct typedefs *this = get_typedef("string", false); + + $$.type_str = (this->type->type_enum == ECPGt_varchar || this->type->type_enum == ECPGt_bytea) ? EMPTY : mm_strdup(this->name); + $$.type_enum = this->type->type_enum; + $$.type_dimension = this->type->type_dimension; + $$.type_index = this->type->type_index; + if (this->type->type_sizeof && strlen(this->type->type_sizeof) != 0) + $$.type_sizeof = this->type->type_sizeof; + else + $$.type_sizeof = cat_str(3, mm_strdup("sizeof("), mm_strdup(this->name), mm_strdup(")")); + + struct_member_list[struct_level] = ECPGstruct_member_dup(this->struct_member_list); + } + } | INTERVAL ecpg_interval { $$.type_enum = ECPGt_interval; diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out new file mode 100644 index 0000000000..8b87580752 --- /dev/null +++ b/src/test/regress/expected/json_sqljson.out @@ -0,0 +1,18 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); +ERROR: JSON_EXISTS() is not yet implemented for the json type +LINE 1: SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_VALUE +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); +ERROR: JSON_VALUE() is not yet implemented for the json type +LINE 1: SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb +-- JSON_QUERY +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); +ERROR: JSON_QUERY() is not yet implemented for the json type +LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out new file mode 100644 index 0000000000..26ba66ee20 --- /dev/null +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -0,0 +1,1042 @@ +-- JSON_EXISTS +SELECT JSON_EXISTS(NULL::jsonb, '$'); + json_exists +------------- + +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb 'null', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); -- FALSE on error + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_EXISTS(jsonb 'null', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[]', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); -- FALSE on error + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{}', '$.a'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + json_exists +------------- + f +(1 row) + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + json_exists +------------- + t +(1 row) + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); + json_exists +------------- + t +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); + json_exists +------------- + f +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); + json_exists +------------- + 1 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); + json_exists +------------- + 0 +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); + json_exists +------------- + true +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); + json_exists +------------- + false +(1 row) + +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +ERROR: cannot cast type boolean to jsonb +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +ERROR: cannot cast type boolean to real +LINE 1: SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); + ^ +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_EXISTS() +LINE 1: ...CT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSO... + ^ +-- JSON_VALUE +SELECT JSON_VALUE(NULL::jsonb, '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$'); + json_value +------------ + true +(1 row) + +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + json_value +------------ + t +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); + json_value +------------ + 123 +(1 row) + +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); +ERROR: SQL/JSON item cannot be cast to target type +SELECT JSON_VALUE(jsonb '1.23', '$'); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); + json_value +------------ + 1.23 +(1 row) + +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "1.23" +SELECT JSON_VALUE(jsonb '"aaa"', '$'); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); + json_value +------------ + aaa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); + json_value +------------ + aa +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); + json_value +------------ + "aaa" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); + json_value +------------ + "\"aaa\"" +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: "aaa" +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); + json_value +------------ + 111 +(1 row) + +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + ?column? +---------- + 357 +(1 row) + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + ?column? +------------ + 03-01-2017 +(1 row) + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE(jsonb '[]', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '{}', '$'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '1', '$.a'); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +ERROR: jsonpath member accessor can only be applied to an object +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); + json_value +------------ + error +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); + json_value +------------ + +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 2 +(1 row) + +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + json_value +------------ + 3 +(1 row) + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_VALUE should return singleton scalar item +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); + json_value +------------ + 0 +(1 row) + +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +ERROR: invalid input syntax for type integer: " " +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 5 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); + json_value +------------ + 1 +(1 row) + +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed +ERROR: cannot specify FORMAT in RETURNING clause of JSON_VALUE() +LINE 1: ...CT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSO... + ^ +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + x | y +---+---- + 0 | -2 + 1 | 2 + 2 | -1 +(3 rows) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); + json_value +------------ + (1,2) +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + json_value +------------ + (1,2) +(1 row) + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); + json_value +------------------------------ + Tue Feb 20 18:34:56 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); + json_value +-------------------------- + Tue Feb 20 18:34:56 2018 +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_value +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- JSON_QUERY +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + json_query | json_query | json_query | json_query | json_query +--------------------+--------------------+--------------------+----------------------+---------------------- + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, null, "2"] | [1, null, "2"] | [1, null, "2"] | [[1, null, "2"]] | [[1, null, "2"]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] +(6 rows) + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + unspec | without | with cond | with uncond | with +--------------------+--------------------+---------------------+----------------------+---------------------- + | | | | + | | | | + null | null | [null] | [null] | [null] + 12.3 | 12.3 | [12.3] | [12.3] | [12.3] + true | true | [true] | [true] | [true] + "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"] + [1, 2, 3] | [1, 2, 3] | [1, 2, 3] | [[1, 2, 3]] | [[1, 2, 3]] + {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}] + | | [1, "2", null, [3]] | [1, "2", null, [3]] | [1, "2", null, [3]] +(9 rows) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); + json_query +------------ + "aaa" +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); + json_query +------------ + aaa +(1 row) + +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +ERROR: invalid input syntax for type json +DETAIL: Token "aaa" is invalid. +CONTEXT: JSON data, line 1: aaa +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + json_query +------------ + \x616161 +(1 row) + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES)... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTE... + ^ +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used +LINE 1: ...N_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTE... + ^ +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + json_query +------------ + [1] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); + json_query +------------ + [] +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +ERROR: no SQL/JSON item +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + json_query +------------ + +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +ERROR: JSON path expression in JSON_QUERY should return singleton item without wrapper +HINT: Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array. +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + json_query +------------ + "empty" +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); + json_query +------------ + [1, +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); + json_query +------------ + [1, 2] +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + json_query +---------------- + \x5b312c20325d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); + json_query +------------ + \x7b7d +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); + json_query +------------ + {} +(1 row) + +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); +ERROR: expected JSON array +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + x | y | list +---+---+-------------- + 0 | 0 | [] + 0 | 1 | [1] + 0 | 2 | [1, 2] + 0 | 3 | [1, 2, 3] + 0 | 4 | [1, 2, 3, 4] + 1 | 0 | [] + 1 | 1 | [1] + 1 | 2 | [1, 2] + 1 | 3 | [1, 2, 3] + 1 | 4 | [1, 2, 3, 4] + 2 | 0 | [] + 2 | 1 | [] + 2 | 2 | [2] + 2 | 3 | [2, 3] + 2 | 4 | [2, 3, 4] + 3 | 0 | [] + 3 | 1 | [] + 3 | 2 | [] + 3 | 3 | [3] + 3 | 4 | [3, 4] + 4 | 0 | [] + 4 | 1 | [] + 4 | 2 | [] + 4 | 3 | [] + 4 | 4 | [4] +(25 rows) + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); + json_query +----------------------------------------------------- + (1,aaa,"[1, ""2"", {}]","{""x"": [1, ""2"", {}]}",) +(1 row) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); + unnest +------------------------ + {"a": 1, "b": ["foo"]} + {"a": 2, "c": {}} + 123 +(3 rows) + +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); + json_query +-------------- + {1,2,NULL,3} +(1 row) + +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + a | t | js | jb | jsa +---+-------------+----+------------+----- + 1 | ["foo", []] | | | + 2 | | | [{}, true] | +(2 rows) + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); + json_query +------------ + 1 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); +ERROR: domain sqljsonb_int_not_null does not allow null values +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + json_query +----------------------------- + "2018-02-21T02:34:56+00:00" +(1 row) + +-- Test constraints +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); +\d test_jsonb_constraints + Table "public.test_jsonb_constraints" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+-------------------------------------------------------------------------------- + js | text | | | + i | integer | | | + x | jsonb | | | JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +Check constraints: + "test_jsonb_constraint1" CHECK (js IS JSON) + "test_jsonb_constraint2" CHECK (JSON_EXISTS(js::jsonb, '$."a"' PASSING i + 5 AS int, i::text AS txt, ARRAY[1, 2, 3] AS arr)) + "test_jsonb_constraint3" CHECK (JSON_VALUE(js::jsonb, '$."a"' RETURNING integer DEFAULT ('12'::text || i)::integer ON EMPTY ERROR ON ERROR) > i) + "test_jsonb_constraint4" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb) + "test_jsonb_constraint5" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C")) + "test_jsonb_constraint6" CHECK (JSON_EXISTS(js::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2) + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + check_clause +-------------------------------------------------------------------------------------------------------------------------- + ((JSON_EXISTS((js)::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2)) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C"))) + ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb)) + ((JSON_VALUE((js)::jsonb, '$."a"' RETURNING integer DEFAULT (('12'::text || i))::integer ON EMPTY ERROR ON ERROR) > i)) + ((js IS JSON)) + (JSON_EXISTS((js)::jsonb, '$."a"' PASSING (i + 5) AS int, (i)::text AS txt, ARRAY[1, 2, 3] AS arr)) +(6 rows) + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +WHERE constraint_name LIKE 'test_jsonb_constraint%'; +ERROR: syntax error at or near "WHERE" +LINE 1: WHERE constraint_name LIKE 'test_jsonb_constraint%'; + ^ +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + pg_get_expr +-------------------------------------------------------------------------------- + JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER) +(1 row) + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint1" +DETAIL: Failing row contains (, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains (1, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('[]'); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ([], null, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2" +DETAIL: Failing row contains ({"b": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint3" +DETAIL: Failing row contains ({"a": 1}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint5" +DETAIL: Failing row contains ({"a": 7}, 1, [1, 2]). +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); +ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint4" +DETAIL: Failing row contains ({"a": 10}, 1, [1, 2]). +DROP TABLE test_jsonb_constraints; +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +ERROR: functions in index expression must be marked IMMUTABLE +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 4df9d8503b..9b0ecf049d 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -103,7 +103,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo # ---------- # Another group of parallel tests (JSON related) # ---------- -test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson +test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson json_sqljson jsonb_sqljson # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql new file mode 100644 index 0000000000..4f30fa46b9 --- /dev/null +++ b/src/test/regress/sql/json_sqljson.sql @@ -0,0 +1,11 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL FORMAT JSON, '$'); + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL FORMAT JSON, '$'); + +-- JSON_QUERY + +SELECT JSON_QUERY(NULL FORMAT JSON, '$'); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql new file mode 100644 index 0000000000..a973de1e57 --- /dev/null +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -0,0 +1,327 @@ +-- JSON_EXISTS + +SELECT JSON_EXISTS(NULL::jsonb, '$'); + +SELECT JSON_EXISTS(jsonb '[]', '$'); +SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$'); + +SELECT JSON_EXISTS(jsonb '1', '$'); +SELECT JSON_EXISTS(jsonb 'null', '$'); +SELECT JSON_EXISTS(jsonb '[]', '$'); + +SELECT JSON_EXISTS(jsonb '1', '$.a'); +SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); -- FALSE on error +SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_EXISTS(jsonb 'null', '$.a'); +SELECT JSON_EXISTS(jsonb '[]', '$.a'); +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); -- FALSE on error +SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a'); +SELECT JSON_EXISTS(jsonb '{}', '$.a'); +SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a'); + +SELECT JSON_EXISTS(jsonb '1', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b'); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b'); + +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y); +SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y); + +-- extension: boolean expressions +SELECT JSON_EXISTS(jsonb '1', '$ > 2'); +SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR); + +-- extension: RETURNING clause +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING bool); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING int); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', '$[1]' RETURNING text); +SELECT JSON_EXISTS(jsonb '1', 'strict $[1]' RETURNING text FALSE ON ERROR); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING jsonb); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4); +SELECT JSON_EXISTS(jsonb '1', '$[0]' RETURNING float4 FORMAT JSON); -- RETURNING FORMAT not allowed + + +-- JSON_VALUE + +SELECT JSON_VALUE(NULL::jsonb, '$'); + +SELECT JSON_VALUE(jsonb 'null', '$'); +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int); + +SELECT JSON_VALUE(jsonb 'true', '$'); +SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool); + +SELECT JSON_VALUE(jsonb '123', '$'); +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234; +SELECT JSON_VALUE(jsonb '123', '$' RETURNING text); +/* jsonb bytea ??? */ +SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1.23', '$'); +SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric); +SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '"aaa"', '$'); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2)); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json); +SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR); +SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234; + +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; + +-- Test NULL checks execution in domain types +CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); + +SELECT JSON_VALUE(jsonb '[]', '$'); +SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '{}', '$'); +SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR); + +SELECT JSON_VALUE(jsonb '1', '$.a'); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR); +SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR); + +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR); +SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed + +SELECT + x, + JSON_VALUE( + jsonb '{"a": 1, "b": 2}', + '$.* ? (@ > $x)' PASSING x AS x + RETURNING int + DEFAULT -1 ON EMPTY + DEFAULT -2 ON ERROR + ) y +FROM + generate_series(0, 2) x; + +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); + +-- Test timestamptz passing and output +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- JSON_QUERY + +SELECT + JSON_QUERY(js, '$'), + JSON_QUERY(js, '$' WITHOUT WRAPPER), + JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER), + JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER), + JSON_QUERY(js, '$' WITH ARRAY WRAPPER) +FROM + (VALUES + (jsonb 'null'), + ('12.3'), + ('true'), + ('"aaa"'), + ('[1, null, "2"]'), + ('{"a": 1, "b": [2]}') + ) foo(js); + +SELECT + JSON_QUERY(js, 'strict $[*]') AS "unspec", + JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without", + JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond", + JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond", + JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with" +FROM + (VALUES + (jsonb '1'), + ('[]'), + ('[null]'), + ('[12.3]'), + ('[true]'), + ('["aaa"]'), + ('[[1, 2, 3]]'), + ('[{"a": 1, "b": [2]}]'), + ('[1, "2", null, [3]]') + ) foo(js); + +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR); + +-- QUOTES behavior should not be specified when WITH WRAPPER used: +-- Should fail +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES); +-- Should succeed +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES); + +SELECT JSON_QUERY(jsonb '[]', '$[*]'); +SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY); + +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR); + +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3)); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea); +SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON); + +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR); + +SELECT + x, y, + JSON_QUERY( + jsonb '[1,2,3,4,5,null]', + '$[*] ? (@ >= $x && @ <= $y)' + PASSING x AS x, y AS y + WITH CONDITIONAL WRAPPER + EMPTY ARRAY ON EMPTY + ) list +FROM + generate_series(0, 4) x, + generate_series(0, 4) y; + +-- Extension: record types returning +CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]); +CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]); + +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); +SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); + +-- Extension: array types returning +SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); +SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[])); + +-- Extension: domain types returning +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null); + +-- Test timestamptz passing and output +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); +SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb); + +-- Test constraints + +CREATE TABLE test_jsonb_constraints ( + js text, + i int, + x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER) + CONSTRAINT test_jsonb_constraint1 + CHECK (js IS JSON) + CONSTRAINT test_jsonb_constraint2 + CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) + CONSTRAINT test_jsonb_constraint3 + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CONSTRAINT test_jsonb_constraint4 + CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') + CONSTRAINT test_jsonb_constraint5 + CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C") + CONSTRAINT test_jsonb_constraint6 + CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2) +); + +\d test_jsonb_constraints + +SELECT check_clause +FROM information_schema.check_constraints +WHERE constraint_name LIKE 'test_jsonb_constraint%' +ORDER BY 1; + +SELECT pg_get_expr(adbin, adrelid) +FROM pg_attrdef +WHERE adrelid = 'test_jsonb_constraints'::regclass +ORDER BY 1; +WHERE constraint_name LIKE 'test_jsonb_constraint%'; + +SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass; + +INSERT INTO test_jsonb_constraints VALUES ('', 1); +INSERT INTO test_jsonb_constraints VALUES ('1', 1); +INSERT INTO test_jsonb_constraints VALUES ('[]'); +INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1); +INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); + +DROP TABLE test_jsonb_constraints; + +-- Test mutabilily od query functions +CREATE TABLE test_jsonb_mutability(js jsonb); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); +DROP TABLE test_jsonb_mutability; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 66823bc2a7..53595ce180 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1240,6 +1240,7 @@ Join JoinCostWorkspace JoinDomain JoinExpr +JsonFuncExpr JoinHashEntry JoinPath JoinPathExtraData @@ -1250,18 +1251,31 @@ JsObject JsValue JsonAggConstructor JsonAggState +JsonArgument JsonArrayAgg JsonArrayConstructor JsonArrayQueryConstructor JsonBaseObjectInfo +JsonBehavior +JsonBehaviorType +JsonCoercion +JsonCoercionState +JsonCommon JsonConstructorExpr JsonConstructorExprState JsonConstructorType JsonEncoding +JsonExpr +JsonExprOp +JsonExprPostEvalState +JsonExprPreEvalState +JsonExprState JsonFormat JsonFormatType JsonHashEntry JsonIsPredicate +JsonItemCoercions +JsonItemCoercionsState JsonIterateStringValuesAction JsonKeyValue JsonLexContext @@ -1279,6 +1293,7 @@ JsonParseContext JsonParseErrorType JsonPath JsonPathBool +JsonPathDatatypeStatus JsonPathExecContext JsonPathExecResult JsonPathGinAddPathItemFunc @@ -1291,10 +1306,15 @@ JsonPathGinPathItem JsonPathItem JsonPathItemType JsonPathKeyword +JsonPathMutableContext JsonPathParseItem JsonPathParseResult JsonPathPredicateCallback JsonPathString +JsonPathVarCallback +JsonPathVariable +JsonPathVariableEvalContext +JsonQuotes JsonReturning JsonScalarExpr JsonSemAction @@ -1311,6 +1331,7 @@ JsonValueExpr JsonValueList JsonValueListIterator JsonValueType +JsonWrapper Jsonb JsonbAggState JsonbContainer -- 2.35.3
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-08-15 08:58  jian he <jian.universality@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: jian he @ 2023-08-15 08:58 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

Hi.
in v11, json_query:
+        The returned <replaceable>data_type</replaceable> has the
same semantics
+        as for constructor functions like <function>json_objectagg</function>;
+        the default returned type is <type>text</type>.
+        The <literal>ON EMPTY</literal> clause specifies the behavior if the
+        <replaceable>path_expression</replaceable> yields no value at all; the
+        default when <literal>ON ERROR</literal> is not specified is
to return a
+        null value.

the default returned type is jsonb?  Also in above quoted second last
line should be <literal>ON EMPTY</literal> ?
Other than that, the doc looks good.





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-08-16 04:27  Amit Langote <amitlangote09@gmail.com>
  parent: jian he <jian.universality@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-08-16 04:27 UTC (permalink / raw)
  To: jian he <jian.universality@gmail.com>; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

On Tue, Aug 15, 2023 at 5:58 PM jian he <jian.universality@gmail.com> wrote:
> Hi.
> in v11, json_query:
> +        The returned <replaceable>data_type</replaceable> has the
> same semantics
> +        as for constructor functions like <function>json_objectagg</function>;
> +        the default returned type is <type>text</type>.
> +        The <literal>ON EMPTY</literal> clause specifies the behavior if the
> +        <replaceable>path_expression</replaceable> yields no value at all; the
> +        default when <literal>ON ERROR</literal> is not specified is
> to return a
> +        null value.
>
> the default returned type is jsonb?

You are correct.

> Also in above quoted second last
> line should be <literal>ON EMPTY</literal> ?

Correct too.

> Other than that, the doc looks good.

Thanks for the review.

I will post a new version after finishing working on a few other
improvements I am working on.

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-08-31 13:51  Erik Rijkers <er@xs4all.nl>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Erik Rijkers @ 2023-08-31 13:51 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; jian he <jian.universality@gmail.com>; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; pgsql-hackers

Op 8/31/23 om 14:57 schreef Amit Langote:
> Hello,
> 
> On Wed, Aug 16, 2023 at 1:27 PM Amit Langote <amitlangote09@gmail.com> wrote:
>> I will post a new version after finishing working on a few other
>> improvements I am working on.
> 
> Sorry about the delay.  Here's a new version.
> 
Hi,

While compiling the new set

[v12-0001-Support-soft-error-handling-during-CoerceViaIO-e.patch]
[v12-0002-SQL-JSON-query-functions.patch]
[v12-0003-JSON_TABLE.patch]
[v12-0004-Claim-SQL-standard-compliance-for-SQL-JSON-featu.patch]

gcc 13.2.0 is sputtering somewhat:

--------------
In function ‘transformJsonFuncExpr’,
     inlined from ‘transformExprRecurse’ at parse_expr.c:374:13:
parse_expr.c:4362:13: warning: ‘contextItemExpr’ may be used 
uninitialized [-Wmaybe-uninitialized]
  4362 |         if (exprType(contextItemExpr) != JSONBOID)
       |             ^~~~~~~~~~~~~~~~~~~~~~~~~
parse_expr.c: In function ‘transformExprRecurse’:
parse_expr.c:4214:21: note: ‘contextItemExpr’ was declared here
  4214 |         Node       *contextItemExpr;
       |                     ^~~~~~~~~~~~~~~
nodeFuncs.c: In function ‘exprSetCollation’:
nodeFuncs.c:1238:25: warning: this statement may fall through 
[-Wimplicit-fallthrough=]
  1238 |                         {
       |                         ^
nodeFuncs.c:1247:17: note: here
  1247 |                 case T_JsonCoercion:
       |                 ^~~~
--------------

Those looks pretty unimportant, but I thought I'd let you know.

Tests (check, check-world and my own) still run fine.

Thanks,

Erik Rijkers






> I found out that llvmjit_expr.c additions have been broken all along,
> I mean since I rewrote the JsonExpr evaluation code to use soft error
> handling back in January or so.  For example, I had made CoerceiViaIO
> evaluation code (EEOP_IOCOERCE ExprEvalStep) invoked by JsonCoercion
> node's evaluation to pass an ErrorSaveContext to the type input
> functions so that any errors result in returning NULL instead of
> throwing the error.  Though the llvmjit_expr.c code was not modified
> to do the same, so the SQL/JSON query functions would return wrong
> results when JITed.  I have made many revisions to the JsonExpr
> expression evaluation itself, not all of which were reflected in the
> llvmjit_expr.c counterparts.   I've fixed all that in the attached.
> 
> I've broken the parts to teach the CoerceViaIO evaluation code to
> handle errors softly into a separate patch attached as 0001.
> 
> Other notable changes in the SQL/JSON query functions patch (now 0002):
> 
> * Significantly rewrote the parser changes to make it a bit more
> readable than before.  My main goal was to separate the code for each
> JSON_EXISTS_OP, JSON_QUERY_OP, and JSON_VALUE_OP such that the
> op-type-specific behaviors are more readily apparent by reading the
> code.
> 
> * Got rid of JsonItemCoercions struct/node, which contained a
> JsonCoercion field to store the coercion expressions for each JSON
> item type that needs to be coerced to the RETURNING type, in favor of
> using List of JsonCoercion nodes.  That resulted in simpler code in
> many places, most notably in the executor / llvmjit_expr.c.
> 





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-09-06 15:01  Alvaro Herrera <alvherre@alvh.no-ip.org>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 2 replies; 300+ messages in thread

From: Alvaro Herrera @ 2023-09-06 15:01 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Erik Rijkers <er@xs4all.nl>; jian he <jian.universality@gmail.com>; Andrew Dunstan <andrew@dunslane.net>; pgsql-hackers

0001 is quite mysterious to me.  I've been reading it but I'm not sure I
grok it, so I don't have anything too intelligent to say about it at
this point.  But here are my thoughts anyway.

Assert()ing that a pointer is not null, and in the next line
dereferencing that pointer, is useless: the process would crash anyway
at the time of dereference, so the Assert() adds no value.  Better to
leave the assert out.  (This appears both in ExecExprEnableErrorSafe and
ExecExprDisableErrorSafe).

Is it not a problem to set just the node type, and not reset the
contents of the node to zeroes, in ExecExprEnableErrorSafe?  I'm not
sure if it's possible to enable error-safe on a node two times with an
error reported in between; would that result in the escontext filled
with junk the second time around?  That might be dangerous.  Maybe a
simple cross-check is to verify (assert) in ExecExprEnableErrorSafe()
that the struct is already all-zeroes, so that if this happens, we'll
get reports about it.  (After all, there are very few nodes that handle
the SOFT_ERROR_OCCURRED case).

Do we need to have the ->details_wanted flag turned on?  Maybe if we're
having ExecExprEnableErrorSafe() as a generic tool, it should receive
the boolean to use as an argument.

Why palloc the escontext always, and not just when
ExecExprEnableErrorSafe is called?  (At Disable time, just memset it to
zero, and next time it is enabled for that node, we don't need to
allocate it again, just set the nodetype.)

ExecExprEnableErrorSafe() is a strange name for this operation.  Maybe
you mean ExecExprEnableSoftErrors()?  Maybe it'd be better to leave it
as NULL initially, so that for the majority of cases we don't even
allocate it.

In 0002 you're adding soft-error support for a bunch of existing
operations, in addition to introducing SQL/JSON query functions.  Maybe
the soft-error stuff should be done separately in a preparatory patch.

I think functions such as populate_array_element() that can now save
soft errors and which currently do not have a return value, should
acquire a convention to let caller know that things failed: maybe return
false if SOFT_ERROR_OCCURRED().  Otherwise it appears that, for instance
populate_array_dim_jsonb() can return happily if an error occurs when
parsing the last element in the array.  Splitting 0002 to have a
preparatory patch where all such soft-error-saving changes are
introduced separately would help review that this is indeed being
handled by all their callers.

-- 
Álvaro Herrera        Breisgau, Deutschland  —  https://www.EnterpriseDB.com/
"Por suerte hoy explotó el califont porque si no me habría muerto
 de aburrido"  (Papelucho)





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-09-12 07:52  Peter Eisentraut <peter@eisentraut.org>
  parent: Alvaro Herrera <alvherre@alvh.no-ip.org>
  1 sibling, 1 reply; 300+ messages in thread

From: Peter Eisentraut @ 2023-09-12 07:52 UTC (permalink / raw)
  To: Alvaro Herrera <alvherre@alvh.no-ip.org>; Amit Langote <amitlangote09@gmail.com>; +Cc: Erik Rijkers <er@xs4all.nl>; jian he <jian.universality@gmail.com>; Andrew Dunstan <andrew@dunslane.net>; pgsql-hackers

On 06.09.23 17:01, Alvaro Herrera wrote:
> Assert()ing that a pointer is not null, and in the next line
> dereferencing that pointer, is useless: the process would crash anyway
> at the time of dereference, so the Assert() adds no value.  Better to
> leave the assert out.

I don't think this is quite correct.  If you dereference a pointer, the 
compiler may assume that it is not null and rearrange code accordingly. 
So it might not crash.  Keeping the assertion would alter that assumption.






^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-09-12 14:43  Tom Lane <tgl@sss.pgh.pa.us>
  parent: Peter Eisentraut <peter@eisentraut.org>
  0 siblings, 0 replies; 300+ messages in thread

From: Tom Lane @ 2023-09-12 14:43 UTC (permalink / raw)
  To: Peter Eisentraut <peter@eisentraut.org>; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>; Amit Langote <amitlangote09@gmail.com>; Erik Rijkers <er@xs4all.nl>; jian he <jian.universality@gmail.com>; Andrew Dunstan <andrew@dunslane.net>; pgsql-hackers

Peter Eisentraut <peter@eisentraut.org> writes:
> On 06.09.23 17:01, Alvaro Herrera wrote:
>> Assert()ing that a pointer is not null, and in the next line
>> dereferencing that pointer, is useless: the process would crash anyway
>> at the time of dereference, so the Assert() adds no value.  Better to
>> leave the assert out.

> I don't think this is quite correct.  If you dereference a pointer, the 
> compiler may assume that it is not null and rearrange code accordingly. 
> So it might not crash.  Keeping the assertion would alter that assumption.

Uh ... only in assert-enabled builds.  If your claim is correct,
this'd result in different behavior in debug and production builds,
which would be even worse.  But I don't believe the claim.
I side with Alvaro's position here: such an assert is unhelpful.

			regards, tom lane





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-09-17 06:37  Erik Rijkers <er@xs4all.nl>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Erik Rijkers @ 2023-09-17 06:37 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: jian he <jian.universality@gmail.com>; Andrew Dunstan <andrew@dunslane.net>; pgsql-hackers

Op 9/14/23 om 10:14 schreef Amit Langote:
> 
> 

Hi Amit,

Just now I built a v14-patched server and I found this crash:

select json_query(jsonb '
{
   "arr": [
     {"arr": [2,3]}
   , {"arr": [4,5]}
   ]
}'
   , '$.arr[*].arr ? (@ <= 3)' returning anyarray  WITH WRAPPER) --crash
;
server closed the connection unexpectedly
	This probably means the server terminated abnormally
	before or while processing the request.
connection to server was lost


Can you have a look?

Thanks,

Erik





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-09-18 10:12  Erik Rijkers <er@xs4all.nl>
  parent: Amit Langote <amitlangote09@gmail.com>
  1 sibling, 1 reply; 300+ messages in thread

From: Erik Rijkers @ 2023-09-18 10:12 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>; jian he <jian.universality@gmail.com>; Andrew Dunstan <andrew@dunslane.net>; pgsql-hackers

Op 9/18/23 om 05:15 schreef Amit Langote:
> On Sun, Sep 17, 2023 at 3:34 PM Erik Rijkers <er@xs4all.nl> wrote:
>> Op 9/14/23 om 10:14 schreef Amit Langote:
>>>
>>>
>>
>> Hi Amit,
>>
>> Just now I built a v14-patched server and I found this crash:
>>
>> select json_query(jsonb '
>> {
>>     "arr": [
>>       {"arr": [2,3]}
>>     , {"arr": [4,5]}
>>     ]
>> }'
>>     , '$.arr[*].arr ? (@ <= 3)' returning anyarray  WITH WRAPPER) --crash
>> ;
>> server closed the connection unexpectedly
>>          This probably means the server terminated abnormally
>>          before or while processing the request.
>> connection to server was lost
> 
> Thanks for the report.
> 
> Attached updated version fixes the crash, but you get an error as is
> to be expected:
> 
> select json_query(jsonb '
> {
>     "arr": [
>       {"arr": [2,3]}
>     , {"arr": [4,5]}
>     ]
> }'
>     , '$.arr[*].arr ? (@ <= 3)' returning anyarray  WITH WRAPPER);
> ERROR:  cannot accept a value of type anyarray
> 
> unlike when using int[]:
> 
> select json_query(jsonb '
> {
>     "arr": [
>       {"arr": [2,3]}
>     , {"arr": [4,5]}
>     ]
> }'
>     , '$.arr[*].arr ? (@ <= 3)' returning int[]  WITH WRAPPER);
>   json_query
> ------------
>   {2,3}
> (1 row)
> 

Thanks, Amit. Alas, there are more: for 'anyarray' I thought I'd 
substitute 'interval', 'int4range', 'int8range', and sure enough they 
all give similar crashes. Patched with v15:

psql -qX -e << SQL
select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}',
   '$.a[*].a?(@<=3)'returning int[] with wrapper --ok
);

select json_query(jsonb'{"a": [{"a": [2,3]}, {"a": [4,5]}]}',
   '$.a[*].a?(@<=3)'returning interval  with wrapper --crash
--'$.a[*].a?(@<=3)'returning int4range with wrapper --crash
--'$.a[*].a?(@<=3)'returning int8range with wrapper --crash
--'$.a[*].a?(@<=3)'returning numeric[] with wrapper --{2,3} =ok
--'$.a[*].a?(@<=3)'returning anyarray  with wrapper --fixed
--'$.a[*].a?(@<=3)'returning anyarray           --null =ok
--'$.a[*].a?(@<=3)'returning int                --null =ok
--'$.a[*].a?(@<=3)'returning int   with wrapper --error =ok
--'$.a[*].a?(@<=3)'returning int[] with wrapper -- {2,3} =ok
);
SQL
=> server closed the connection unexpectedly, etc

Because those first three tries gave a crash (*all three*), I'm a bit 
worried there may be many more.

I am sorry to be bothering you with these somewhat idiotic SQL 
statements but I suppose somehow it needs to be made more solid.

Thanks!

Erik





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-09-18 10:20  Amit Langote <amitlangote09@gmail.com>
  parent: Erik Rijkers <er@xs4all.nl>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-09-18 10:20 UTC (permalink / raw)
  To: Erik Rijkers <er@xs4all.nl>; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; pgsql-hackers; jian he <jian.universality@gmail.com>

Hi Erik,

On Mon, Sep 18, 2023 at 19:09 Erik Rijkers <er@xs4all.nl> wrote:

> Op 9/18/23 om 05:15 schreef Amit Langote:
> > On Sun, Sep 17, 2023 at 3:34 PM Erik Rijkers <er@xs4all.nl> wrote:
> >> Op 9/14/23 om 10:14 schreef Amit Langote:
> >>>
> >>>
> >>
> >> Hi Amit,
> >>
> >> Just now I built a v14-patched server and I found this crash:
> >>
> >> select json_query(jsonb '
> >> {
> >>     "arr": [
> >>       {"arr": [2,3]}
> >>     , {"arr": [4,5]}
> >>     ]
> >> }'
> >>     , '$.arr[*].arr ? (@ <= 3)' returning anyarray  WITH WRAPPER)
> --crash
> >> ;
> >> server closed the connection unexpectedly
> >>          This probably means the server terminated abnormally
> >>          before or while processing the request.
> >> connection to server was lost
> >
> > Thanks for the report.
> >
> > Attached updated version fixes the crash, but you get an error as is
> > to be expected:
> >
> > select json_query(jsonb '
> > {
> >     "arr": [
> >       {"arr": [2,3]}
> >     , {"arr": [4,5]}
> >     ]
> > }'
> >     , '$.arr[*].arr ? (@ <= 3)' returning anyarray  WITH WRAPPER);
> > ERROR:  cannot accept a value of type anyarray
> >
> > unlike when using int[]:
> >
> > select json_query(jsonb '
> > {
> >     "arr": [
> >       {"arr": [2,3]}
> >     , {"arr": [4,5]}
> >     ]
> > }'
> >     , '$.arr[*].arr ? (@ <= 3)' returning int[]  WITH WRAPPER);
> >   json_query
> > ------------
> >   {2,3}
> > (1 row)
> >
>
> Thanks, Amit. Alas, there are more: for 'anyarray' I thought I'd
> substitute 'interval', 'int4range', 'int8range', and sure enough they
> all give similar crashes. Patched with v15:
>
> psql -qX -e << SQL
> select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}',
>    '$.a[*].a?(@<=3)'returning int[] with wrapper --ok
> );
>
> select json_query(jsonb'{"a": [{"a": [2,3]}, {"a": [4,5]}]}',
>    '$.a[*].a?(@<=3)'returning interval  with wrapper --crash
> --'$.a[*].a?(@<=3)'returning int4range with wrapper --crash
> --'$.a[*].a?(@<=3)'returning int8range with wrapper --crash
> --'$.a[*].a?(@<=3)'returning numeric[] with wrapper --{2,3} =ok
> --'$.a[*].a?(@<=3)'returning anyarray  with wrapper --fixed
> --'$.a[*].a?(@<=3)'returning anyarray           --null =ok
> --'$.a[*].a?(@<=3)'returning int                --null =ok
> --'$.a[*].a?(@<=3)'returning int   with wrapper --error =ok
> --'$.a[*].a?(@<=3)'returning int[] with wrapper -- {2,3} =ok
> );
> SQL
> => server closed the connection unexpectedly, etc
>
> Because those first three tries gave a crash (*all three*), I'm a bit
> worried there may be many more.
>
> I am sorry to be bothering you with these somewhat idiotic SQL
> statements but I suppose somehow it needs to be made more solid.


No, thanks for your testing.  I’ll look into these.

>

^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-09-18 11:14  Erik Rijkers <er@xs4all.nl>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Erik Rijkers @ 2023-09-18 11:14 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; pgsql-hackers; jian he <jian.universality@gmail.com>

Op 9/18/23 om 12:20 schreef Amit Langote:
> Hi Erik,
> 
>> I am sorry to be bothering you with these somewhat idiotic SQL
>> statements but I suppose somehow it needs to be made more solid.
> 

For 60 datatypes, I ran this statement:

select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}',
  '$.a[*].a?(@<=3)'returning ${datatype} with wrapper
);

against a 17devel server (a0a5) with json v15 patches and caught the 
output, incl. 30+ crashes, in the attached .txt. I hope that's useful.


Erik

Attachments:

crash_20120918.txt (text/plain, 24.1K) download --------------------------------------------------------------- [int4 START ] -- -- datatype [int4] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning int4 with wrapper ); ERROR: cannot cast jsonb array to type integer --------------------------------------------------------------- [int4 END ] -- --------------------------------------------------------------- [bit START ] -- -- datatype [bit] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning bit with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [bit END ] -- --------------------------------------------------------------- [bigint START ] -- -- datatype [bigint] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning bigint with wrapper ); ERROR: cannot cast jsonb array to type bigint --------------------------------------------------------------- [bigint END ] -- --------------------------------------------------------------- [bit varying START ] -- -- datatype [bit varying] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning bit varying with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [bit varying END ] -- --------------------------------------------------------------- [boolean START ] -- -- datatype [boolean] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning boolean with wrapper ); ERROR: cannot cast jsonb array to type boolean --------------------------------------------------------------- [boolean END ] -- --------------------------------------------------------------- [box START ] -- -- datatype [box] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning box with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [box END ] -- --------------------------------------------------------------- [bytea START ] -- -- datatype [bytea] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning bytea with wrapper ); json_query ---------------- \x5b322c20335d (1 row) --------------------------------------------------------------- [bytea END ] -- --------------------------------------------------------------- [character varying(5) START ] -- -- datatype [character varying(5)] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning character varying(5) with wrapper ); json_query ------------ [2, 3 (1 row) --------------------------------------------------------------- [character varying(5) END ] -- --------------------------------------------------------------- [cidr START ] -- -- datatype [cidr] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning cidr with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [cidr END ] -- --------------------------------------------------------------- [circle START ] -- -- datatype [circle] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning circle with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [circle END ] -- --------------------------------------------------------------- [date START ] -- -- datatype [date] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning date with wrapper ); json_query ------------ 2000-01-01 (1 row) --------------------------------------------------------------- [date END ] -- --------------------------------------------------------------- [double precision START ] -- -- datatype [double precision] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning double precision with wrapper ); ERROR: cannot cast jsonb array to type double precision --------------------------------------------------------------- [double precision END ] -- --------------------------------------------------------------- [float START ] -- -- datatype [float] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning float with wrapper ); ERROR: cannot cast jsonb array to type double precision --------------------------------------------------------------- [float END ] -- --------------------------------------------------------------- [inet START ] -- -- datatype [inet] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning inet with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [inet END ] -- --------------------------------------------------------------- [interval START ] -- -- datatype [interval] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning interval with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [interval END ] -- --------------------------------------------------------------- [json START ] -- -- datatype [json] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning json with wrapper ); json_query ------------ [2, 3] (1 row) --------------------------------------------------------------- [json END ] -- --------------------------------------------------------------- [jsonb START ] -- -- datatype [jsonb] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning jsonb with wrapper ); json_query ------------ [2, 3] (1 row) --------------------------------------------------------------- [jsonb END ] -- --------------------------------------------------------------- [line START ] -- -- datatype [line] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning line with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [line END ] -- --------------------------------------------------------------- [lseg START ] -- -- datatype [lseg] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning lseg with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [lseg END ] -- --------------------------------------------------------------- [macaddr START ] -- -- datatype [macaddr] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning macaddr with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [macaddr END ] -- --------------------------------------------------------------- [macaddr8 START ] -- -- datatype [macaddr8] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning macaddr8 with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [macaddr8 END ] -- --------------------------------------------------------------- [money START ] -- -- datatype [money] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning money with wrapper ); json_query ------------ $0.00 (1 row) --------------------------------------------------------------- [money END ] -- --------------------------------------------------------------- [numeric START ] -- -- datatype [numeric] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning numeric with wrapper ); ERROR: cannot cast jsonb array to type numeric --------------------------------------------------------------- [numeric END ] -- --------------------------------------------------------------- [path START ] -- -- datatype [path] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning path with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [path END ] -- --------------------------------------------------------------- [pg_lsn START ] -- -- datatype [pg_lsn] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning pg_lsn with wrapper ); json_query ------------ 0/0 (1 row) --------------------------------------------------------------- [pg_lsn END ] -- --------------------------------------------------------------- [point START ] -- -- datatype [point] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning point with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [point END ] -- --------------------------------------------------------------- [polygon START ] -- -- datatype [polygon] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning polygon with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [polygon END ] -- --------------------------------------------------------------- [real START ] -- -- datatype [real] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning real with wrapper ); ERROR: cannot cast jsonb array to type real --------------------------------------------------------------- [real END ] -- --------------------------------------------------------------- [smallint START ] -- -- datatype [smallint] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning smallint with wrapper ); ERROR: cannot cast jsonb array to type smallint --------------------------------------------------------------- [smallint END ] -- --------------------------------------------------------------- [serial START ] -- -- datatype [serial] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning serial with wrapper ); ERROR: type "serial" does not exist LINE 2: '$.a[*].a?(@<=3)'returning serial with wrapper ^ --------------------------------------------------------------- [serial END ] -- --------------------------------------------------------------- [text START ] -- -- datatype [text] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning text with wrapper ); json_query ------------ [2, 3] (1 row) --------------------------------------------------------------- [text END ] -- --------------------------------------------------------------- [time START ] -- -- datatype [time] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning time with wrapper ); json_query ------------ 00:00:00 (1 row) --------------------------------------------------------------- [time END ] -- --------------------------------------------------------------- [time without time zone START ] -- -- datatype [time without time zone] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning time without time zone with wrapper ); json_query ------------ 00:00:00 (1 row) --------------------------------------------------------------- [time without time zone END ] -- --------------------------------------------------------------- [time with time zone START ] -- -- datatype [time with time zone] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning time with time zone with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [time with time zone END ] -- --------------------------------------------------------------- [timestamp START ] -- -- datatype [timestamp] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning timestamp with wrapper ); json_query --------------------- 2000-01-01 00:00:00 (1 row) --------------------------------------------------------------- [timestamp END ] -- --------------------------------------------------------------- [timestamp without time zone START ] -- -- datatype [timestamp without time zone] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning timestamp without time zone with wrapper ); json_query --------------------- 2000-01-01 00:00:00 (1 row) --------------------------------------------------------------- [timestamp without time zone END ] -- --------------------------------------------------------------- [timestamp with time zone START ] -- -- datatype [timestamp with time zone] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning timestamp with time zone with wrapper ); json_query ------------------------ 2000-01-01 01:00:00+01 (1 row) --------------------------------------------------------------- [timestamp with time zone END ] -- --------------------------------------------------------------- [tsquery START ] -- -- datatype [tsquery] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning tsquery with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [tsquery END ] -- --------------------------------------------------------------- [tsvector START ] -- -- datatype [tsvector] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning tsvector with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [tsvector END ] -- --------------------------------------------------------------- [uuid START ] -- -- datatype [uuid] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning uuid with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [uuid END ] -- --------------------------------------------------------------- [txid_snapshot START ] -- -- datatype [txid_snapshot] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning txid_snapshot with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [txid_snapshot END ] -- --------------------------------------------------------------- [xml START ] -- -- datatype [xml] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning xml with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [xml END ] -- --------------------------------------------------------------- [decimal START ] -- -- datatype [decimal] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning decimal with wrapper ); ERROR: cannot cast jsonb array to type numeric --------------------------------------------------------------- [decimal END ] -- --------------------------------------------------------------- [char(4) START ] -- -- datatype [char(4)] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning char(4) with wrapper ); json_query ------------ [2, (1 row) --------------------------------------------------------------- [char(4) END ] -- --------------------------------------------------------------- [character(4) START ] -- -- datatype [character(4)] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning character(4) with wrapper ); json_query ------------ [2, (1 row) --------------------------------------------------------------- [character(4) END ] -- --------------------------------------------------------------- [bit(3) START ] -- -- datatype [bit(3)] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning bit(3) with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [bit(3) END ] -- --------------------------------------------------------------- [int4range START ] -- -- datatype [int4range] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning int4range with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [int4range END ] -- --------------------------------------------------------------- [int4multirange START ] -- -- datatype [int4multirange] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning int4multirange with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [int4multirange END ] -- --------------------------------------------------------------- [int8range START ] -- -- datatype [int8range] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning int8range with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [int8range END ] -- --------------------------------------------------------------- [int8multirange START ] -- -- datatype [int8multirange] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning int8multirange with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [int8multirange END ] -- --------------------------------------------------------------- [numrange START ] -- -- datatype [numrange] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning numrange with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [numrange END ] -- --------------------------------------------------------------- [nummultirange START ] -- -- datatype [nummultirange] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning nummultirange with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [nummultirange END ] -- --------------------------------------------------------------- [tsrange START ] -- -- datatype [tsrange] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning tsrange with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [tsrange END ] -- --------------------------------------------------------------- [tsmultirange START ] -- -- datatype [tsmultirange] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning tsmultirange with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [tsmultirange END ] -- --------------------------------------------------------------- [tstzrange START ] -- -- datatype [tstzrange] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning tstzrange with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [tstzrange END ] -- --------------------------------------------------------------- [tstzmultirange START ] -- -- datatype [tstzmultirange] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning tstzmultirange with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [tstzmultirange END ] -- --------------------------------------------------------------- [daterange START ] -- -- datatype [daterange] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning daterange with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [daterange END ] -- --------------------------------------------------------------- [datemultirange START ] -- -- datatype [datemultirange] select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}', '$.a[*].a?(@<=3)'returning datemultirange with wrapper ); server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost --------------------------------------------------------------- [datemultirange END ] --
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-09-18 11:53  Erik Rijkers <er@xs4all.nl>
  parent: Erik Rijkers <er@xs4all.nl>
  0 siblings, 1 reply; 300+ messages in thread

From: Erik Rijkers @ 2023-09-18 11:53 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; pgsql-hackers; jian he <jian.universality@gmail.com>

Op 9/18/23 om 13:14 schreef Erik Rijkers:
> Op 9/18/23 om 12:20 schreef Amit Langote:
>> Hi Erik,
>>
>>> I am sorry to be bothering you with these somewhat idiotic SQL
>>> statements but I suppose somehow it needs to be made more solid.
>>
> 
> For 60 datatypes, I ran this statement:
> 
> select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}',
>   '$.a[*].a?(@<=3)'returning ${datatype} with wrapper
> );
>
> against a 17devel server (a0a5) with json v15 patches and caught the 
> output, incl. 30+ crashes, in the attached .txt. I hope that's useful.
> 

and FYI: None of these crashes occur when I leave off the 'WITH WRAPPER' 
clause.

> 
> Erik
> 





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-09-19 10:18  Alvaro Herrera <alvherre@alvh.no-ip.org>
  parent: Amit Langote <amitlangote09@gmail.com>
  1 sibling, 1 reply; 300+ messages in thread

From: Alvaro Herrera @ 2023-09-19 10:18 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Erik Rijkers <er@xs4all.nl>; jian he <jian.universality@gmail.com>; Andrew Dunstan <andrew@dunslane.net>; pgsql-hackers

0001: I wonder why you used Node for the ErrorSaveContext pointer
instead of the specific struct you want.  I propose the attached, for
some extra type-safety.  Or did you have a reason to do it that way?

-- 
Álvaro Herrera        Breisgau, Deutschland  —  https://www.EnterpriseDB.com/
"How amazing is that? I call it a night and come back to find that a bug has
been identified and patched while I sleep."                (Robert Davidson)
               http://archives.postgresql.org/pgsql-sql/2006-03/msg00378.php

Attachments:

0001-0001-fixup-use-struct-ErrorSaveContext-not-Node.notpatch (text/plain, 4.3K) download From 4cd888cd7abbfb156bad26bc94658be3e286bf1f Mon Sep 17 00:00:00 2001 From: Alvaro Herrera <alvherre@alvh.no-ip.org> Date: Tue, 19 Sep 2023 12:18:03 +0200 Subject: [PATCH] 0001 fixup: use struct ErrorSaveContext, not Node --- src/backend/jit/llvm/llvmjit.c | 4 ++-- src/backend/jit/llvm/llvmjit_expr.c | 2 +- src/backend/jit/llvm/llvmjit_types.c | 2 +- src/include/executor/execExpr.h | 3 ++- src/include/jit/llvmjit.h | 2 +- src/include/nodes/execnodes.h | 3 ++- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/backend/jit/llvm/llvmjit.c b/src/backend/jit/llvm/llvmjit.c index ef6c8361b4..431d4511c5 100644 --- a/src/backend/jit/llvm/llvmjit.c +++ b/src/backend/jit/llvm/llvmjit.c @@ -85,7 +85,7 @@ LLVMTypeRef StructExprState; LLVMTypeRef StructAggState; LLVMTypeRef StructAggStatePerGroupData; LLVMTypeRef StructAggStatePerTransData; -LLVMTypeRef StructNode; +LLVMTypeRef StructErrorSaveContext; LLVMValueRef AttributeTemplate; @@ -1035,7 +1035,7 @@ llvm_create_types(void) StructAggState = llvm_pg_var_type("StructAggState"); StructAggStatePerGroupData = llvm_pg_var_type("StructAggStatePerGroupData"); StructAggStatePerTransData = llvm_pg_var_type("StructAggStatePerTransData"); - StructNode = llvm_pg_var_type("StructNode"); + StructErrorSaveContext = llvm_pg_var_type("StructErrorSaveContext"); AttributeTemplate = LLVMGetNamedFunction(llvm_types_module, "AttributeTemplate"); } diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 20c79dda9f..59e20ebcbd 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -1361,7 +1361,7 @@ llvm_compile_expr(ExprState *state) v_params[2] = l_int32_const(ioparam); v_params[3] = l_int32_const(-1); v_params[4] = l_ptr_const(op->d.iocoerce.escontext, - l_ptr(StructNode)); + l_ptr(StructErrorSaveContext)); /* * InputFunctionCallSafe() will writes directly into * *op->resvalue. diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c index 36f526b374..e1e9625038 100644 --- a/src/backend/jit/llvm/llvmjit_types.c +++ b/src/backend/jit/llvm/llvmjit_types.c @@ -67,7 +67,7 @@ TupleTableSlot StructTupleTableSlot; HeapTupleTableSlot StructHeapTupleTableSlot; MinimalTupleTableSlot StructMinimalTupleTableSlot; TupleDescData StructTupleDescData; -Node StructNode; +ErrorSaveContext StructErrorSaveContext; /* diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index 25d3c02dba..8dd92f8fc0 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -16,6 +16,7 @@ #include "executor/nodeAgg.h" #include "nodes/execnodes.h" +#include "nodes/miscnodes.h" /* forward references to avoid circularity */ struct ExprEvalStep; @@ -417,7 +418,7 @@ typedef struct ExprEvalStep /* lookup and call info for result type's input function */ FmgrInfo *finfo_in; Oid typioparam; - Node *escontext; + ErrorSaveContext *escontext; } iocoerce; /* for EEOP_SQLVALUEFUNCTION */ diff --git a/src/include/jit/llvmjit.h b/src/include/jit/llvmjit.h index e8cdc96b2b..c22ba3787f 100644 --- a/src/include/jit/llvmjit.h +++ b/src/include/jit/llvmjit.h @@ -79,7 +79,7 @@ extern PGDLLIMPORT LLVMTypeRef StructExprState; extern PGDLLIMPORT LLVMTypeRef StructAggState; extern PGDLLIMPORT LLVMTypeRef StructAggStatePerTransData; extern PGDLLIMPORT LLVMTypeRef StructAggStatePerGroupData; -extern PGDLLIMPORT LLVMTypeRef StructNode; +extern PGDLLIMPORT LLVMTypeRef StructErrorSaveContext; extern PGDLLIMPORT LLVMValueRef AttributeTemplate; diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 57e4ca1c63..cc4ed9279c 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -34,6 +34,7 @@ #include "fmgr.h" #include "lib/ilist.h" #include "lib/pairingheap.h" +#include "nodes/miscnodes.h" #include "nodes/params.h" #include "nodes/plannodes.h" #include "nodes/tidbitmap.h" @@ -134,7 +135,7 @@ typedef struct ExprState * Used by the callers of ExecInitExprRec() to pass the ErrorSaveContext * that they want the expression's code to use. */ - Node *escontext; + ErrorSaveContext *escontext; } ExprState; -- 2.39.2
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-09-19 10:37  jian he <jian.universality@gmail.com>
  parent: Erik Rijkers <er@xs4all.nl>
  0 siblings, 1 reply; 300+ messages in thread

From: jian he @ 2023-09-19 10:37 UTC (permalink / raw)
  To: Erik Rijkers <er@xs4all.nl>; +Cc: Amit Langote <amitlangote09@gmail.com>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; pgsql-hackers

On Mon, Sep 18, 2023 at 7:51 PM Erik Rijkers <er@xs4all.nl> wrote:
>
> and FYI: None of these crashes occur when I leave off the 'WITH WRAPPER'
> clause.
>
> >
> > Erik
> >

if specify with wrapper, then default behavior is keep quotes, so
jexpr->omit_quotes will be false, which make val_string NULL.
in ExecEvalJsonExprCoercion: InputFunctionCallSafe, val_string is
NULL, flinfo->fn_strict is true, it will return:  *op->resvalue =
(Datum) 0. but at the same time  *op->resnull is still false!

if not specify with wrapper, then JsonPathQuery will return NULL.
(because after apply the path_expression, cannot multiple SQL/JSON
items)

select json_query(jsonb'{"a":[{"a":3},{"a":[4,5]}]}','$.a[*].a?(@<=3)'
 returning int4range);
also make server crash, because default is KEEP QUOTES, so in
ExecEvalJsonExprCoercion jexpr->omit_quotes will be false.
val_string will be NULL again as mentioned above.

another funny case:
create domain domain_int4range int4range;
select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}','$.a[*].a?(@<=3)'
 returning domain_int4range with wrapper);

should I expect it to return  [2,4)  ?
 -------------------
https://www.postgresql.org/docs/current/extend-type-system.html#EXTEND-TYPES-POLYMORPHIC
>>  When the return value of a function is declared as a polymorphic type, there must be at least one argument position that is also
>> polymorphic, and the actual data type(s) supplied for the polymorphic arguments determine the actual result type for that call.

select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}','$.a[*].a?(@<=3)'
returning anyrange);
should fail. Now it returns NULL. Maybe we can validate it in
transformJsonFuncExpr?
-------------------





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-09-19 12:00  Amit Langote <amitlangote09@gmail.com>
  parent: jian he <jian.universality@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-09-19 12:00 UTC (permalink / raw)
  To: jian he <jian.universality@gmail.com>; +Cc: Erik Rijkers <er@xs4all.nl>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; pgsql-hackers

On Tue, Sep 19, 2023 at 7:37 PM jian he <jian.universality@gmail.com> wrote:
> On Mon, Sep 18, 2023 at 7:51 PM Erik Rijkers <er@xs4all.nl> wrote:
> >
> > and FYI: None of these crashes occur when I leave off the 'WITH WRAPPER'
> > clause.
> >
> > >
> > > Erik
> > >
>
> if specify with wrapper, then default behavior is keep quotes, so
> jexpr->omit_quotes will be false, which make val_string NULL.
> in ExecEvalJsonExprCoercion: InputFunctionCallSafe, val_string is
> NULL, flinfo->fn_strict is true, it will return:  *op->resvalue =
> (Datum) 0. but at the same time  *op->resnull is still false!
>
> if not specify with wrapper, then JsonPathQuery will return NULL.
> (because after apply the path_expression, cannot multiple SQL/JSON
> items)
>
> select json_query(jsonb'{"a":[{"a":3},{"a":[4,5]}]}','$.a[*].a?(@<=3)'
>  returning int4range);
> also make server crash, because default is KEEP QUOTES, so in
> ExecEvalJsonExprCoercion jexpr->omit_quotes will be false.
> val_string will be NULL again as mentioned above.

That's right.

> another funny case:
> create domain domain_int4range int4range;
> select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}','$.a[*].a?(@<=3)'
>  returning domain_int4range with wrapper);
>
> should I expect it to return  [2,4)  ?

This is what you'll get with v16 that I just posted.

>  -------------------
> https://www.postgresql.org/docs/current/extend-type-system.html#EXTEND-TYPES-POLYMORPHIC
> >>  When the return value of a function is declared as a polymorphic type, there must be at least one argument position that is also
> >> polymorphic, and the actual data type(s) supplied for the polymorphic arguments determine the actual result type for that call.
>
> select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}','$.a[*].a?(@<=3)'
> returning anyrange);
> should fail. Now it returns NULL. Maybe we can validate it in
> transformJsonFuncExpr?
> -------------------

I'm not sure whether we should make the parser complain about the
weird types being specified in RETURNING.  The NULL you get in the
above example is because of the following error:

select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}','$.a[*].a?(@<=3)'
returning anyrange error on error);
ERROR:  JSON path expression in JSON_QUERY should return singleton
item without wrapper
HINT:  Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array.

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-09-19 12:34  Erik Rijkers <er@xs4all.nl>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Erik Rijkers @ 2023-09-19 12:34 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: jian he <jian.universality@gmail.com>; Andrew Dunstan <andrew@dunslane.net>; pgsql-hackers

Op 9/19/23 om 13:56 schreef Amit Langote:
> On Tue, Sep 19, 2023 at 7:18 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
>> 0001: I wonder why you used Node for the ErrorSaveContext pointer
>> instead of the specific struct you want.  I propose the attached, for
>> some extra type-safety.  Or did you have a reason to do it that way?
> 
> No reason other than that most other headers use Node.  I agree that
> making an exception for this patch might be better, so I've
> incorporated your patch into 0001.
> 
> I've also updated the query functions patch (0003) to address the
> crashing bug reported by Erik.  Essentially, I made the coercion step
> of JSON_QUERY to always use json_populate_type() when WITH WRAPPER is
> used.  You might get funny errors with ERROR OR ERROR for many types
> when used in RETURNING, but at least there should no longer be any
> crashes.
> 

Indeed, with v16 those crashes are gone.

Some lesser evil: gcc 13.2.0 gave some warnings, slightly different in 
assert vs non-assert build.

--- assert build:

-- [2023.09.19 14:06:35 json_table2/0] make core: make --quiet -j 4
In file included from ../../../src/include/postgres.h:45,
                  from parse_expr.c:16:
In function ‘transformJsonFuncExpr’,
     inlined from ‘transformExprRecurse’ at parse_expr.c:374:13:
parse_expr.c:4355:22: warning: ‘jsexpr’ may be used uninitialized 
[-Wmaybe-uninitialized]
  4355 |         Assert(jsexpr->formatted_expr);
../../../src/include/c.h:864:23: note: in definition of macro ‘Assert’
   864 |                 if (!(condition)) \
       |                       ^~~~~~~~~
parse_expr.c: In function ‘transformExprRecurse’:
parse_expr.c:4212:21: note: ‘jsexpr’ was declared here
  4212 |         JsonExpr   *jsexpr;
       |                     ^~~~~~


--- non-assert build:

-- [2023.09.19 14:11:03 json_table2/1] make core: make --quiet -j 4
In function ‘transformJsonFuncExpr’,
     inlined from ‘transformExprRecurse’ at parse_expr.c:374:13:
parse_expr.c:4356:28: warning: ‘jsexpr’ may be used uninitialized 
[-Wmaybe-uninitialized]
  4356 |         if (exprType(jsexpr->formatted_expr) != JSONBOID)
       |                      ~~~~~~^~~~~~~~~~~~~~~~
parse_expr.c: In function ‘transformExprRecurse’:
parse_expr.c:4212:21: note: ‘jsexpr’ was declared here
  4212 |         JsonExpr   *jsexpr;
       |                     ^~~~~~


Thank you,

Erik






^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-09-20 03:07  Amit Langote <amitlangote09@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-09-20 03:07 UTC (permalink / raw)
  To: jian he <jian.universality@gmail.com>; +Cc: Erik Rijkers <er@xs4all.nl>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; pgsql-hackers

On Tue, Sep 19, 2023 at 9:00 PM Amit Langote <amitlangote09@gmail.com> wrote:
> On Tue, Sep 19, 2023 at 7:37 PM jian he <jian.universality@gmail.com> wrote:
> >  -------------------
> > https://www.postgresql.org/docs/current/extend-type-system.html#EXTEND-TYPES-POLYMORPHIC
> > >>  When the return value of a function is declared as a polymorphic type, there must be at least one argument position that is also
> > >> polymorphic, and the actual data type(s) supplied for the polymorphic arguments determine the actual result type for that call.
> >
> > select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}','$.a[*].a?(@<=3)'
> > returning anyrange);
> > should fail. Now it returns NULL. Maybe we can validate it in
> > transformJsonFuncExpr?
> > -------------------
>
> I'm not sure whether we should make the parser complain about the
> weird types being specified in RETURNING.

Sleeping over this, maybe adding the following to
transformJsonOutput() does make sense?

+   if (get_typtype(ret->typid) == TYPTYPE_PSEUDO)
+       ereport(ERROR,
+               errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+               errmsg("returning pseudo-types is not supported in
SQL/JSON functions"));
+

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-09-20 19:14  Andrew Dunstan <andrew@dunslane.net>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 2 replies; 300+ messages in thread

From: Andrew Dunstan @ 2023-09-20 19:14 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; jian he <jian.universality@gmail.com>; +Cc: Erik Rijkers <er@xs4all.nl>; Alvaro Herrera <alvherre@alvh.no-ip.org>; pgsql-hackers


On 2023-09-19 Tu 23:07, Amit Langote wrote:
> On Tue, Sep 19, 2023 at 9:00 PM Amit Langote<amitlangote09@gmail.com>  wrote:
>> On Tue, Sep 19, 2023 at 7:37 PM jian he<jian.universality@gmail.com>  wrote:
>>>   -------------------
>>> https://www.postgresql.org/docs/current/extend-type-system.html#EXTEND-TYPES-POLYMORPHIC
>>>>>   When the return value of a function is declared as a polymorphic type, there must be at least one argument position that is also
>>>>> polymorphic, and the actual data type(s) supplied for the polymorphic arguments determine the actual result type for that call.
>>> select json_query(jsonb'{"a":[{"a":[2,3]},{"a":[4,5]}]}','$.a[*].a?(@<=3)'
>>> returning anyrange);
>>> should fail. Now it returns NULL. Maybe we can validate it in
>>> transformJsonFuncExpr?
>>> -------------------
>> I'm not sure whether we should make the parser complain about the
>> weird types being specified in RETURNING.
> Sleeping over this, maybe adding the following to
> transformJsonOutput() does make sense?
>
> +   if (get_typtype(ret->typid) == TYPTYPE_PSEUDO)
> +       ereport(ERROR,
> +               errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
> +               errmsg("returning pseudo-types is not supported in
> SQL/JSON functions"));
> +
>

Seems reasonable.


cheers


andrew


--
Andrew Dunstan
EDB:https://www.enterprisedb.com

^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-09-21 08:57  Alvaro Herrera <alvherre@alvh.no-ip.org>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Alvaro Herrera @ 2023-09-21 08:57 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Andrew Dunstan <andrew@dunslane.net>; jian he <jian.universality@gmail.com>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

I keep looking at 0001, and in the struct definition I think putting the
escontext at the bottom is not great, because there's a comment a few
lines above that says "XXX: following fields only needed during
"compilation"), could be thrown away afterwards".  This comment is not
strictly true, because innermost_caseval is actually used by
array_map(); yet it seems that ->escontext should appear before that
comment.

However, if you put it before steps_len, it would push members steps_len
and steps_alloc beyond the struct's first cache line(*).  If those
struct members are critical for expression init performance, then maybe
it's not a good tradeoff.  I don't know if this was struct laid out
carefully with that consideration in mind or not.

Also, ->escontext's own comment in ExprState seems to be saying too much
and not saying enough.  I would reword it as "For expression nodes that
support soft errors.  NULL if caller wants them thrown instead".  The
shortest I could make so that it fits in a single is "For nodes that can
error softly. NULL if caller wants them thrown", or "For
soft-error-enabled nodes.  NULL if caller wants errors thrown".  Not
sure if those are good enough, or just make the comment the whole four
lines ...


(*) This is what pahole says about the struct as 0001 would put it:

struct ExprState {
    NodeTag                    type;                 /*     0     4 */
    uint8                      flags;                /*     4     1 */
    _Bool                      resnull;              /*     5     1 */

    /* XXX 2 bytes hole, try to pack */

    Datum                      resvalue;             /*     8     8 */
    TupleTableSlot *           resultslot;           /*    16     8 */
    struct ExprEvalStep *      steps;                /*    24     8 */
    ExprStateEvalFunc          evalfunc;             /*    32     8 */
    Expr *                     expr;                 /*    40     8 */
    void *                     evalfunc_private;     /*    48     8 */
    int                        steps_len;            /*    56     4 */
    int                        steps_alloc;          /*    60     4 */
    /* --- cacheline 1 boundary (64 bytes) --- */
    struct PlanState *         parent;               /*    64     8 */
    ParamListInfo              ext_params;           /*    72     8 */
    Datum *                    innermost_caseval;    /*    80     8 */
    _Bool *                    innermost_casenull;   /*    88     8 */
    Datum *                    innermost_domainval;  /*    96     8 */
    _Bool *                    innermost_domainnull; /*   104     8 */
    ErrorSaveContext *         escontext;            /*   112     8 */

    /* size: 120, cachelines: 2, members: 18 */
    /* sum members: 118, holes: 1, sum holes: 2 */
    /* last cacheline: 56 bytes */
};

-- 
Álvaro Herrera        Breisgau, Deutschland  —  https://www.EnterpriseDB.com/
"Nunca se desea ardientemente lo que solo se desea por razón" (F. Alexandre)





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-09-27 14:23  Erik Rijkers <er@xs4all.nl>
  parent: Amit Langote <amitlangote09@gmail.com>
  1 sibling, 1 reply; 300+ messages in thread

From: Erik Rijkers @ 2023-09-27 14:23 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: Andrew Dunstan <andrew@dunslane.net>; jian he <jian.universality@gmail.com>; pgsql-hackers

Op 9/27/23 om 15:55 schreef Amit Langote:
> On Thu, Sep 21, 2023 at 9:41 PM Amit Langote <amitlangote09@gmail.com> wrote:

I don't knoe, maybe it's worthwhile to fix this (admittedly trivial) 
fail in the tests? It's been there for a while.

Thanks,

Erik

Attachments:

regression.diffs (text/plain, 2.2K) download diff -U3 /home/aardvark/pg_stuff/pg_sandbox/pgsql.json_table/src/test/regress/expected/jsonb_sqljson.out /home/aardvark/pg_stuff/pg_sandbox/pgsql.json_table/src/test/regress/results/jsonb_sqljson.out --- /home/aardvark/pg_stuff/pg_sandbox/pgsql.json_table/src/test/regress/expected/jsonb_sqljson.out 2023-09-27 16:10:52.361367183 +0200 +++ /home/aardvark/pg_stuff/pg_sandbox/pgsql.json_table/src/test/regress/results/jsonb_sqljson.out 2023-09-27 16:18:50.929382498 +0200 @@ -994,14 +994,14 @@ FROM information_schema.check_constraints WHERE constraint_name LIKE 'test_jsonb_constraint%' ORDER BY 1; - check_clause --------------------------------------------------------------------------------------------------------------------------- - ((JSON_EXISTS((js)::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2)) - ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C"))) - ((JSON_QUERY((js)::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb)) - ((JSON_VALUE((js)::jsonb, '$."a"' RETURNING integer DEFAULT (('12'::text || i))::integer ON EMPTY ERROR ON ERROR) > i)) - ((js IS JSON)) - (JSON_EXISTS((js)::jsonb, '$."a"' PASSING (i + 5) AS int, (i)::text AS txt, ARRAY[1, 2, 3] AS arr)) + check_clause +------------------------------------------------------------------------------------------------------------------------ + (JSON_EXISTS((js)::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2) + (JSON_QUERY((js)::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C")) + (JSON_QUERY((js)::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb) + (JSON_VALUE((js)::jsonb, '$."a"' RETURNING integer DEFAULT (('12'::text || i))::integer ON EMPTY ERROR ON ERROR) > i) + (js IS JSON) + JSON_EXISTS((js)::jsonb, '$."a"' PASSING (i + 5) AS int, (i)::text AS txt, ARRAY[1, 2, 3] AS arr) (6 rows) SELECT pg_get_expr(adbin, adrelid)
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-09-28 11:04  Alvaro Herrera <alvherre@alvh.no-ip.org>
  parent: Amit Langote <amitlangote09@gmail.com>
  1 sibling, 1 reply; 300+ messages in thread

From: Alvaro Herrera @ 2023-09-28 11:04 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Andrew Dunstan <andrew@dunslane.net>; jian he <jian.universality@gmail.com>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

On 2023-Sep-27, Amit Langote wrote:

> Maybe the following is better:
> 
> +   /*
> +    * For expression nodes that support soft errors.  Should be set to NULL
> +    * before calling ExecInitExprRec() if the caller wants errors thrown.
> +    */
> 
> ...as in the attached.

That's good.

> Alvaro, do you think your concern regarding escontext not being in the
> right spot in the ExprState struct is addressed?  It doesn't seem very
> critical to me to place it in the struct's 1st cacheline, because
> escontext is not accessed in performance critical paths such as during
> expression evaluation, especially with the latest version.  (It would
> get accessed during evaluation with previous versions.)
> 
> If so, I'd like to move ahead with committing it.

Yeah, looks OK to me in v21.

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-02 04:24  Amit Langote <amitlangote09@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  1 sibling, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-10-02 04:24 UTC (permalink / raw)
  To: Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: Andrew Dunstan <andrew@dunslane.net>; jian he <jian.universality@gmail.com>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

On Fri, Sep 29, 2023 at 1:57 PM Amit Langote <amitlangote09@gmail.com> wrote:
> On Thu, Sep 28, 2023 at 8:04 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
> > On 2023-Sep-27, Amit Langote wrote:
> > > Maybe the following is better:
> > >
> > > +   /*
> > > +    * For expression nodes that support soft errors.  Should be set to NULL
> > > +    * before calling ExecInitExprRec() if the caller wants errors thrown.
> > > +    */
> > >
> > > ...as in the attached.
> >
> > That's good.
> >
> > > Alvaro, do you think your concern regarding escontext not being in the
> > > right spot in the ExprState struct is addressed?  It doesn't seem very
> > > critical to me to place it in the struct's 1st cacheline, because
> > > escontext is not accessed in performance critical paths such as during
> > > expression evaluation, especially with the latest version.  (It would
> > > get accessed during evaluation with previous versions.)
> > >
> > > If so, I'd like to move ahead with committing it.
> >
> > Yeah, looks OK to me in v21.
>
> Thanks.  I will push the attached 0001 shortly.

Pushed this 30 min ago (no email on -committers yet!) and am looking
at the following llvm crash reported by buildfarm animal pogona [1]:

#0  __pthread_kill_implementation (threadid=<optimized out>,
signo=signo@entry=6, no_tid=no_tid@entry=0) at
./nptl/pthread_kill.c:44
44 ./nptl/pthread_kill.c: No such file or directory.
#0  __pthread_kill_implementation (threadid=<optimized out>,
signo=signo@entry=6, no_tid=no_tid@entry=0) at
./nptl/pthread_kill.c:44
#1  0x00007f5bcebcb15f in __pthread_kill_internal (signo=6,
threadid=<optimized out>) at ./nptl/pthread_kill.c:78
#2  0x00007f5bceb7d472 in __GI_raise (sig=sig@entry=6) at
../sysdeps/posix/raise.c:26
#3  0x00007f5bceb674b2 in __GI_abort () at ./stdlib/abort.c:79
#4  0x00007f5bceb673d5 in __assert_fail_base (fmt=0x7f5bcecdbdc8
"%s%s%s:%u: %s%sAssertion `%s' failed.\\n%n",
assertion=assertion@entry=0x7f5bc1336419 "(i >= FTy->getNumParams() ||
FTy->getParamType(i) == Args[i]->getType()) && \\"Calling a function
with a bad signature!\\"", file=file@entry=0x7f5bc1336051
"/home/bf/src/llvm-project-5/llvm/lib/IR/Instructions.cpp",
line=line@entry=299, function=function@entry=0x7f5bc13362af "void
llvm::CallInst::init(llvm::FunctionType *, llvm::Value *,
ArrayRef<llvm::Value *>, ArrayRef<llvm::OperandBundleDef>, const
llvm::Twine &)") at ./assert/assert.c:92
#5  0x00007f5bceb763a2 in __assert_fail (assertion=0x7f5bc1336419 "(i
>= FTy->getNumParams() || FTy->getParamType(i) == Args[i]->getType())
&& \\"Calling a function with a bad signature!\\"",
file=0x7f5bc1336051
"/home/bf/src/llvm-project-5/llvm/lib/IR/Instructions.cpp", line=299,
function=0x7f5bc13362af "void llvm::CallInst::init(llvm::FunctionType
*, llvm::Value *, ArrayRef<llvm::Value *>,
ArrayRef<llvm::OperandBundleDef>, const llvm::Twine &)") at
./assert/assert.c:101
#6  0x00007f5bc110f138 in llvm::CallInst::init (this=0x557a91f3e508,
FTy=0x557a91ed9ae0, Func=0x557a91f8be88, Args=..., Bundles=...,
NameStr=...) at
/home/bf/src/llvm-project-5/llvm/lib/IR/Instructions.cpp:297
#7  0x00007f5bc0fa579d in llvm::CallInst::CallInst
(this=0x557a91f3e508, Ty=0x557a91ed9ae0, Func=0x557a91f8be88,
Args=..., Bundles=..., NameStr=..., InsertBefore=0x0) at
/home/bf/src/llvm-project-5/llvm/include/llvm/IR/Instructions.h:1934
#8  0x00007f5bc0fa538c in llvm::CallInst::Create (Ty=0x557a91ed9ae0,
Func=0x557a91f8be88, Args=..., Bundles=..., NameStr=...,
InsertBefore=0x0) at
/home/bf/src/llvm-project-5/llvm/include/llvm/IR/Instructions.h:1444
#9  0x00007f5bc0fa51f9 in llvm::IRBuilder<llvm::ConstantFolder,
llvm::IRBuilderDefaultInserter>::CreateCall (this=0x557a91f9c6a0,
FTy=0x557a91ed9ae0, Callee=0x557a91f8be88, Args=..., Name=...,
FPMathTag=0x0) at
/home/bf/src/llvm-project-5/llvm/include/llvm/IR/IRBuilder.h:1669
#10 0x00007f5bc100edda in llvm::IRBuilder<llvm::ConstantFolder,
llvm::IRBuilderDefaultInserter>::CreateCall (this=0x557a91f9c6a0,
Callee=0x557a91f8be88, Args=..., Name=..., FPMathTag=0x0) at
/home/bf/src/llvm-project-5/llvm/include/llvm/IR/IRBuilder.h:1663
#11 0x00007f5bc100714e in LLVMBuildCall (B=0x557a91f9c6a0,
Fn=0x557a91f8be88, Args=0x7ffde6fa0b50, NumArgs=6, Name=0x7f5bc30b648c
"funccall_iocoerce_in_safe") at
/home/bf/src/llvm-project-5/llvm/lib/IR/Core.cpp:2964
#12 0x00007f5bc30af861 in llvm_compile_expr (state=0x557a91fbeac0) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/jit/llvm/llvmjit_expr.c:1373
#13 0x0000557a915992db in jit_compile_expr
(state=state@entry=0x557a91fbeac0) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/jit/jit.c:177
#14 0x0000557a9123071d in ExecReadyExpr
(state=state@entry=0x557a91fbeac0) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/executor/execExpr.c:880
#15 0x0000557a912340d7 in ExecBuildProjectionInfo
(targetList=0x557a91fa6b58, econtext=<optimized out>, slot=<optimized
out>, parent=parent@entry=0x557a91f430a8,
inputDesc=inputDesc@entry=0x0) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/executor/execExpr.c:484
#16 0x0000557a9124e61e in ExecAssignProjectionInfo
(planstate=planstate@entry=0x557a91f430a8,
inputDesc=inputDesc@entry=0x0) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/executor/execUtils.c:547
#17 0x0000557a91274961 in ExecInitNestLoop
(node=node@entry=0x557a91f9e5d8, estate=estate@entry=0x557a91f425a0,
eflags=<optimized out>, eflags@entry=33) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/executor/nodeNestloop.c:308
#18 0x0000557a9124760f in ExecInitNode (node=0x557a91f9e5d8,
estate=estate@entry=0x557a91f425a0, eflags=eflags@entry=33) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/executor/execProcnode.c:298
#19 0x0000557a91255d39 in ExecInitAgg (node=node@entry=0x557a91f91540,
estate=estate@entry=0x557a91f425a0, eflags=eflags@entry=33) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/executor/nodeAgg.c:3306
#20 0x0000557a912476bf in ExecInitNode (node=0x557a91f91540,
estate=estate@entry=0x557a91f425a0, eflags=eflags@entry=33) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/executor/execProcnode.c:341
#21 0x0000557a912770c3 in ExecInitSort
(node=node@entry=0x557a91f9e850, estate=estate@entry=0x557a91f425a0,
eflags=eflags@entry=33) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/executor/nodeSort.c:265
#22 0x0000557a91247667 in ExecInitNode
(node=node@entry=0x557a91f9e850, estate=estate@entry=0x557a91f425a0,
eflags=eflags@entry=33) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/executor/execProcnode.c:321
#23 0x0000557a912402f5 in InitPlan (eflags=33,
queryDesc=0x557a91fa6fb8) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/executor/execMain.c:968
#24 standard_ExecutorStart (queryDesc=queryDesc@entry=0x557a91fa6fb8,
eflags=33, eflags@entry=1) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/executor/execMain.c:266
#25 0x0000557a912403c9 in ExecutorStart
(queryDesc=queryDesc@entry=0x557a91fa6fb8, eflags=1) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/executor/execMain.c:145
#26 0x0000557a911c2153 in ExplainOnePlan
(plannedstmt=plannedstmt@entry=0x557a91fa6ea8, into=into@entry=0x0,
es=es@entry=0x557a91f932e8,
queryString=queryString@entry=0x557a91dbd650 "EXPLAIN (COSTS
OFF)\\nSELECT DISTINCT (i || '/' || j)::pg_lsn f\\n  FROM
generate_series(1, 10) i,\\n       generate_series(1, 10) j,\\n
generate_series(1, 5) k\\n  WHERE i <= 10 AND j > 0 AND j <= 10\\n
O"..., params=params@entry=0x0, queryEnv=queryEnv@entry=0x0,
planduration=0x7ffde6fa1258, bufusage=0x0) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/commands/explain.c:590
#27 0x0000557a911c23b2 in ExplainOneQuery (query=<optimized out>,
cursorOptions=cursorOptions@entry=2048, into=into@entry=0x0,
es=es@entry=0x557a91f932e8, queryString=0x557a91dbd650 "EXPLAIN (COSTS
OFF)\\nSELECT DISTINCT (i || '/' || j)::pg_lsn f\\n  FROM
generate_series(1, 10) i,\\n       generate_series(1, 10) j,\\n
generate_series(1, 5) k\\n  WHERE i <= 10 AND j > 0 AND j <= 10\\n
O"..., params=params@entry=0x0, queryEnv=0x0) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/commands/explain.c:419
#28 0x0000557a911c2ddb in ExplainQuery
(pstate=pstate@entry=0x557a91f3eb18, stmt=stmt@entry=0x557a91e881d0,
params=params@entry=0x0, dest=dest@entry=0x557a91f3ea88) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/include/nodes/nodes.h:193
#29 0x0000557a91413811 in standard_ProcessUtility
(pstmt=0x557a91e88280, queryString=0x557a91dbd650 "EXPLAIN (COSTS
OFF)\\nSELECT DISTINCT (i || '/' || j)::pg_lsn f\\n  FROM
generate_series(1, 10) i,\\n       generate_series(1, 10) j,\\n
generate_series(1, 5) k\\n  WHERE i <= 10 AND j > 0 AND j <= 10\\n
O"..., readOnlyTree=<optimized out>, context=PROCESS_UTILITY_TOPLEVEL,
params=0x0, queryEnv=0x0, dest=0x557a91f3ea88, qc=0x7ffde6fa1500) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/tcop/utility.c:870
#30 0x0000557a91413ed9 in ProcessUtility
(pstmt=pstmt@entry=0x557a91e88280, queryString=<optimized out>,
readOnlyTree=<optimized out>,
context=context@entry=PROCESS_UTILITY_TOPLEVEL, params=<optimized
out>, queryEnv=<optimized out>, dest=0x557a91f3ea88,
qc=0x7ffde6fa1500) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/tcop/utility.c:530
#31 0x0000557a91411537 in PortalRunUtility
(portal=portal@entry=0x557a91e35970, pstmt=0x557a91e88280,
isTopLevel=true, setHoldSnapshot=setHoldSnapshot@entry=true,
dest=dest@entry=0x557a91f3ea88, qc=qc@entry=0x7ffde6fa1500) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/tcop/pquery.c:1158
#32 0x0000557a914119a4 in FillPortalStore
(portal=portal@entry=0x557a91e35970, isTopLevel=isTopLevel@entry=true)
at /home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/include/nodes/nodes.h:193
#33 0x0000557a91411d6d in PortalRun
(portal=portal@entry=0x557a91e35970,
count=count@entry=9223372036854775807,
isTopLevel=isTopLevel@entry=true, run_once=run_once@entry=true,
dest=dest@entry=0x557a91e88900, altdest=altdest@entry=0x557a91e88900,
qc=0x7ffde6fa1700) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/tcop/pquery.c:763
#34 0x0000557a9140d65f in exec_simple_query
(query_string=query_string@entry=0x557a91dbd650 "EXPLAIN (COSTS
OFF)\\nSELECT DISTINCT (i || '/' || j)::pg_lsn f\\n  FROM
generate_series(1, 10) i,\\n       generate_series(1, 10) j,\\n
generate_series(1, 5) k\\n  WHERE i <= 10 AND j > 0 AND j <= 10\\n
O"...) at /home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/tcop/postgres.c:1272
#35 0x0000557a9140e305 in PostgresMain (dbname=<optimized out>,
username=<optimized out>) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/tcop/postgres.c:4652
#36 0x0000557a91372bf0 in BackendRun (port=0x557a91de8730) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/postmaster/postmaster.c:4439
#37 BackendStartup (port=0x557a91de8730) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/postmaster/postmaster.c:4167
#38 ServerLoop () at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/postmaster/postmaster.c:1781
#39 0x0000557a9137488e in PostmasterMain (argc=argc@entry=8,
argv=argv@entry=0x557a91d7cc10) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/postmaster/postmaster.c:1465
#40 0x0000557a912a001e in main (argc=8, argv=0x557a91d7cc10) at
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/main/main.c:198
$1 = {si_signo = 6, si_errno = 0, si_code = -6, _sifields = {_pad =
{3110875, 1000, 0 <repeats 26 times>}, _kill = {si_pid = 3110875,
si_uid = 1000}, _timer = {si_tid = 3110875, si_overrun = 1000,
si_sigval = {sival_int = 0, sival_ptr = 0x0}}, _rt = {si_pid =
3110875, si_uid = 1000, si_sigval = {sival_int = 0, sival_ptr = 0x0}},
_sigchld = {si_pid = 3110875, si_uid = 1000, si_status = 0, si_utime =
0, si_stime = 0}, _sigfault = {si_addr = 0x3e8002f77db, _addr_lsb = 0,
_addr_bnd = {_lower = 0x0, _upper = 0x0}}, _sigpoll = {si_band =
4294970406875, si_fd = 0}, _sigsys = {_call_addr = 0x3e8002f77db,
_syscall = 0, _arch = 0}}}

This seems to me to be complaining about the following addition:

+                   {
+                       Oid         ioparam = op->d.iocoerce.typioparam;
+                       LLVMValueRef v_params[6];
+                       LLVMValueRef v_success;
+
+                       v_params[0] = l_ptr_const(op->d.iocoerce.finfo_in,
+                                                 l_ptr(StructFmgrInfo));
+                       v_params[1] = v_output;
+                       v_params[2] = l_oid_const(lc, ioparam);
+                       v_params[3] = l_int32_const(lc, -1);
+                       v_params[4] = l_ptr_const(op->d.iocoerce.escontext,
+
l_ptr(StructErrorSaveContext));

-                   LLVMBuildStore(b, v_retval, v_resvaluep);
+                       /*
+                        * InputFunctionCallSafe() will write directly into
+                        * *op->resvalue.
+                        */
+                       v_params[5] = v_resvaluep;
+
+                       v_success = LLVMBuildCall(b, llvm_pg_func(mod,
"InputFunctionCallSafe"),
+                                                 v_params, lengthof(v_params),
+                                                 "funccall_iocoerce_in_safe");
+
+                       /*
+                        * Return null if InputFunctionCallSafe() encountered
+                        * an error.
+                        */
+                       v_resnullp = LLVMBuildICmp(b, LLVMIntEQ, v_success,
+                                                  l_sbool_const(0), "");
+                   }

--
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

[1] https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=pogona&dt=2023-10-02%2003%3A50%3A20





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-02 05:26  Amit Langote <amitlangote09@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-10-02 05:26 UTC (permalink / raw)
  To: Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers; jian he <jian.universality@gmail.com>

On Mon, Oct 2, 2023 at 1:24 PM Amit Langote <amitlangote09@gmail.com> wrote:
> Pushed this 30 min ago (no email on -committers yet!) and am looking
> at the following llvm crash reported by buildfarm animal pogona [1]:
>
> #4  0x00007f5bceb673d5 in __assert_fail_base (fmt=0x7f5bcecdbdc8
> "%s%s%s:%u: %s%sAssertion `%s' failed.\\n%n",
> assertion=assertion@entry=0x7f5bc1336419 "(i >= FTy->getNumParams() ||
> FTy->getParamType(i) == Args[i]->getType()) && \\"Calling a function
> with a bad signature!\\"", file=file@entry=0x7f5bc1336051
> "/home/bf/src/llvm-project-5/llvm/lib/IR/Instructions.cpp",
> line=line@entry=299, function=function@entry=0x7f5bc13362af "void
> llvm::CallInst::init(llvm::FunctionType *, llvm::Value *,
> ArrayRef<llvm::Value *>, ArrayRef<llvm::OperandBundleDef>, const
> llvm::Twine &)") at ./assert/assert.c:92
> #5  0x00007f5bceb763a2 in __assert_fail (assertion=0x7f5bc1336419 "(i
> >= FTy->getNumParams() || FTy->getParamType(i) == Args[i]->getType())
> && \\"Calling a function with a bad signature!\\"",
> file=0x7f5bc1336051
> "/home/bf/src/llvm-project-5/llvm/lib/IR/Instructions.cpp", line=299,
> function=0x7f5bc13362af "void llvm::CallInst::init(llvm::FunctionType
> *, llvm::Value *, ArrayRef<llvm::Value *>,
> ArrayRef<llvm::OperandBundleDef>, const llvm::Twine &)") at
> ./assert/assert.c:101
> #6  0x00007f5bc110f138 in llvm::CallInst::init (this=0x557a91f3e508,
> FTy=0x557a91ed9ae0, Func=0x557a91f8be88, Args=..., Bundles=...,
> NameStr=...) at
> /home/bf/src/llvm-project-5/llvm/lib/IR/Instructions.cpp:297
> #7  0x00007f5bc0fa579d in llvm::CallInst::CallInst
> (this=0x557a91f3e508, Ty=0x557a91ed9ae0, Func=0x557a91f8be88,
> Args=..., Bundles=..., NameStr=..., InsertBefore=0x0) at
> /home/bf/src/llvm-project-5/llvm/include/llvm/IR/Instructions.h:1934
> #8  0x00007f5bc0fa538c in llvm::CallInst::Create (Ty=0x557a91ed9ae0,
> Func=0x557a91f8be88, Args=..., Bundles=..., NameStr=...,
> InsertBefore=0x0) at
> /home/bf/src/llvm-project-5/llvm/include/llvm/IR/Instructions.h:1444
> #9  0x00007f5bc0fa51f9 in llvm::IRBuilder<llvm::ConstantFolder,
> llvm::IRBuilderDefaultInserter>::CreateCall (this=0x557a91f9c6a0,
> FTy=0x557a91ed9ae0, Callee=0x557a91f8be88, Args=..., Name=...,
> FPMathTag=0x0) at
> /home/bf/src/llvm-project-5/llvm/include/llvm/IR/IRBuilder.h:1669
> #10 0x00007f5bc100edda in llvm::IRBuilder<llvm::ConstantFolder,
> llvm::IRBuilderDefaultInserter>::CreateCall (this=0x557a91f9c6a0,
> Callee=0x557a91f8be88, Args=..., Name=..., FPMathTag=0x0) at
> /home/bf/src/llvm-project-5/llvm/include/llvm/IR/IRBuilder.h:1663
> #11 0x00007f5bc100714e in LLVMBuildCall (B=0x557a91f9c6a0,
> Fn=0x557a91f8be88, Args=0x7ffde6fa0b50, NumArgs=6, Name=0x7f5bc30b648c
> "funccall_iocoerce_in_safe") at
> /home/bf/src/llvm-project-5/llvm/lib/IR/Core.cpp:2964
> #12 0x00007f5bc30af861 in llvm_compile_expr (state=0x557a91fbeac0) at
>
/home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/jit/llvm/llvmjit_expr.c:1373
>
> This seems to me to be complaining about the following addition:
>
> +                   {
> +                       Oid         ioparam = op->d.iocoerce.typioparam;
> +                       LLVMValueRef v_params[6];
> +                       LLVMValueRef v_success;
> +
> +                       v_params[0] = l_ptr_const(op->d.iocoerce.finfo_in,
> +                                                 l_ptr(StructFmgrInfo));
> +                       v_params[1] = v_output;
> +                       v_params[2] = l_oid_const(lc, ioparam);
> +                       v_params[3] = l_int32_const(lc, -1);
> +                       v_params[4] =
l_ptr_const(op->d.iocoerce.escontext,
> +
> l_ptr(StructErrorSaveContext));
>
> -                   LLVMBuildStore(b, v_retval, v_resvaluep);
> +                       /*
> +                        * InputFunctionCallSafe() will write directly
into
> +                        * *op->resvalue.
> +                        */
> +                       v_params[5] = v_resvaluep;
> +
> +                       v_success = LLVMBuildCall(b, llvm_pg_func(mod,
> "InputFunctionCallSafe"),
> +                                                 v_params,
lengthof(v_params),
> +
 "funccall_iocoerce_in_safe");
> +
> +                       /*
> +                        * Return null if InputFunctionCallSafe()
encountered
> +                        * an error.
> +                        */
> +                       v_resnullp = LLVMBuildICmp(b, LLVMIntEQ,
v_success,
> +                                                  l_sbool_const(0), "");
> +                   }

Although most animals except pogona looked fine, I've decided to revert the
patch for now.

IIUC, LLVM is complaining that the code in the above block is not passing
the arguments of InputFunctionCallSafe() using the correct types.  I'm not
exactly sure which particular argument is not handled correctly in the
above code, but perhaps it's:


+                       v_params[1] = v_output;

which maps to char *str argument of InputFunctionCallSafe().  v_output is
set in the code preceding the above block as follows:

                    /* and call output function (can never return NULL) */
                    v_output = LLVMBuildCall(b, v_fn_out, &v_fcinfo_out,
                                             1, "funccall_coerce_out");

I thought that it would be fine to pass it as-is to the call of
InputFunctionCallSafe() given that v_fn_out is a call to a function that
returns char *, but perhaps not.


-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-03 13:11  Amit Langote <amitlangote09@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-10-03 13:11 UTC (permalink / raw)
  To: Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers; jian he <jian.universality@gmail.com>

On Mon, Oct 2, 2023 at 2:26 PM Amit Langote <amitlangote09@gmail.com> wrote:
> On Mon, Oct 2, 2023 at 1:24 PM Amit Langote <amitlangote09@gmail.com> wrote:
> > Pushed this 30 min ago (no email on -committers yet!) and am looking
> > at the following llvm crash reported by buildfarm animal pogona [1]:
> >
> > #4  0x00007f5bceb673d5 in __assert_fail_base (fmt=0x7f5bcecdbdc8
> > "%s%s%s:%u: %s%sAssertion `%s' failed.\\n%n",
> > assertion=assertion@entry=0x7f5bc1336419 "(i >= FTy->getNumParams() ||
> > FTy->getParamType(i) == Args[i]->getType()) && \\"Calling a function
> > with a bad signature!\\"", file=file@entry=0x7f5bc1336051
> > "/home/bf/src/llvm-project-5/llvm/lib/IR/Instructions.cpp",
> > line=line@entry=299, function=function@entry=0x7f5bc13362af "void
> > llvm::CallInst::init(llvm::FunctionType *, llvm::Value *,
> > ArrayRef<llvm::Value *>, ArrayRef<llvm::OperandBundleDef>, const
> > llvm::Twine &)") at ./assert/assert.c:92
> > #5  0x00007f5bceb763a2 in __assert_fail (assertion=0x7f5bc1336419 "(i
> > >= FTy->getNumParams() || FTy->getParamType(i) == Args[i]->getType())
> > && \\"Calling a function with a bad signature!\\"",
> > file=0x7f5bc1336051
> > "/home/bf/src/llvm-project-5/llvm/lib/IR/Instructions.cpp", line=299,
> > function=0x7f5bc13362af "void llvm::CallInst::init(llvm::FunctionType
> > *, llvm::Value *, ArrayRef<llvm::Value *>,
> > ArrayRef<llvm::OperandBundleDef>, const llvm::Twine &)") at
> > ./assert/assert.c:101
> > #6  0x00007f5bc110f138 in llvm::CallInst::init (this=0x557a91f3e508,
> > FTy=0x557a91ed9ae0, Func=0x557a91f8be88, Args=..., Bundles=...,
> > NameStr=...) at
> > /home/bf/src/llvm-project-5/llvm/lib/IR/Instructions.cpp:297
> > #7  0x00007f5bc0fa579d in llvm::CallInst::CallInst
> > (this=0x557a91f3e508, Ty=0x557a91ed9ae0, Func=0x557a91f8be88,
> > Args=..., Bundles=..., NameStr=..., InsertBefore=0x0) at
> > /home/bf/src/llvm-project-5/llvm/include/llvm/IR/Instructions.h:1934
> > #8  0x00007f5bc0fa538c in llvm::CallInst::Create (Ty=0x557a91ed9ae0,
> > Func=0x557a91f8be88, Args=..., Bundles=..., NameStr=...,
> > InsertBefore=0x0) at
> > /home/bf/src/llvm-project-5/llvm/include/llvm/IR/Instructions.h:1444
> > #9  0x00007f5bc0fa51f9 in llvm::IRBuilder<llvm::ConstantFolder,
> > llvm::IRBuilderDefaultInserter>::CreateCall (this=0x557a91f9c6a0,
> > FTy=0x557a91ed9ae0, Callee=0x557a91f8be88, Args=..., Name=...,
> > FPMathTag=0x0) at
> > /home/bf/src/llvm-project-5/llvm/include/llvm/IR/IRBuilder.h:1669
> > #10 0x00007f5bc100edda in llvm::IRBuilder<llvm::ConstantFolder,
> > llvm::IRBuilderDefaultInserter>::CreateCall (this=0x557a91f9c6a0,
> > Callee=0x557a91f8be88, Args=..., Name=..., FPMathTag=0x0) at
> > /home/bf/src/llvm-project-5/llvm/include/llvm/IR/IRBuilder.h:1663
> > #11 0x00007f5bc100714e in LLVMBuildCall (B=0x557a91f9c6a0,
> > Fn=0x557a91f8be88, Args=0x7ffde6fa0b50, NumArgs=6, Name=0x7f5bc30b648c
> > "funccall_iocoerce_in_safe") at
> > /home/bf/src/llvm-project-5/llvm/lib/IR/Core.cpp:2964
> > #12 0x00007f5bc30af861 in llvm_compile_expr (state=0x557a91fbeac0) at
> > /home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/jit/llvm/llvmjit_expr.c:1373
> >
> > This seems to me to be complaining about the following addition:
> >
> > +                   {
> > +                       Oid         ioparam = op->d.iocoerce.typioparam;
> > +                       LLVMValueRef v_params[6];
> > +                       LLVMValueRef v_success;
> > +
> > +                       v_params[0] = l_ptr_const(op->d.iocoerce.finfo_in,
> > +                                                 l_ptr(StructFmgrInfo));
> > +                       v_params[1] = v_output;
> > +                       v_params[2] = l_oid_const(lc, ioparam);
> > +                       v_params[3] = l_int32_const(lc, -1);
> > +                       v_params[4] = l_ptr_const(op->d.iocoerce.escontext,
> > +
> > l_ptr(StructErrorSaveContext));
> >
> > -                   LLVMBuildStore(b, v_retval, v_resvaluep);
> > +                       /*
> > +                        * InputFunctionCallSafe() will write directly into
> > +                        * *op->resvalue.
> > +                        */
> > +                       v_params[5] = v_resvaluep;
> > +
> > +                       v_success = LLVMBuildCall(b, llvm_pg_func(mod,
> > "InputFunctionCallSafe"),
> > +                                                 v_params, lengthof(v_params),
> > +                                                 "funccall_iocoerce_in_safe");
> > +
> > +                       /*
> > +                        * Return null if InputFunctionCallSafe() encountered
> > +                        * an error.
> > +                        */
> > +                       v_resnullp = LLVMBuildICmp(b, LLVMIntEQ, v_success,
> > +                                                  l_sbool_const(0), "");
> > +                   }
>
> Although most animals except pogona looked fine, I've decided to revert the patch for now.
>
> IIUC, LLVM is complaining that the code in the above block is not passing the arguments of InputFunctionCallSafe() using the correct types.  I'm not exactly sure which particular argument is not handled correctly in the above code, but perhaps it's:
>
>
> +                       v_params[1] = v_output;
>
> which maps to char *str argument of InputFunctionCallSafe().  v_output is set in the code preceding the above block as follows:
>
>                     /* and call output function (can never return NULL) */
>                     v_output = LLVMBuildCall(b, v_fn_out, &v_fcinfo_out,
>                                              1, "funccall_coerce_out");
>
> I thought that it would be fine to pass it as-is to the call of InputFunctionCallSafe() given that v_fn_out is a call to a function that returns char *, but perhaps not.

OK, I think I could use some help from LLVM experts here.

So, the LLVM code involving setting up a call to
InputFunctionCallSafe() seems to *work*, but BF animal pogona's debug
build (?) is complaining that the parameter types don't match up.
Parameters are set up as follows:

+                   {
+                       Oid         ioparam = op->d.iocoerce.typioparam;
+                       LLVMValueRef v_params[6];
+                       LLVMValueRef v_success;
+
+                       v_params[0] = l_ptr_const(op->d.iocoerce.finfo_in,
+                                                 l_ptr(StructFmgrInfo));
+                       v_params[1] = v_output;
+                       v_params[2] = l_oid_const(lc, ioparam);
+                       v_params[3] = l_int32_const(lc, -1);
+                       v_params[4] = l_ptr_const(op->d.iocoerce.escontext,
+
l_ptr(StructErrorSaveContext));
 +                       /*
+                        * InputFunctionCallSafe() will write directly into
+                        * *op->resvalue.
+                        */
+                       v_params[5] = v_resvaluep;
+
+                       v_success = LLVMBuildCall(b, llvm_pg_func(mod,
"InputFunctionCallSafe"),
+                                                 v_params, lengthof(v_params),
+                                                 "funccall_iocoerce_in_safe");
+
+                       /*
+                        * Return null if InputFunctionCallSafe() encountered
+                        * an error.
+                        */
+                       v_resnullp = LLVMBuildICmp(b, LLVMIntEQ, v_success,
+                                                  l_sbool_const(0), "");
+                   }

And here's InputFunctionCallSafe's signature:

bool
InputFunctionCallSafe(FmgrInfo *flinfo, Datum d,
                      Oid typioparam, int32 typmod,
                      fmNodePtr escontext,
                      Datum *result)

I suspected that assignment to either param[1] or param[5] might be wrong.

param[1] in InputFunctionCallSafe's signature is char *, but the code
assigns it v_output, which is an LLVMValueRef for the output
function's output, a Datum, so I thought LLVM's type checker is
complaining that I'm trying to pass the Datum to char * without
appropriate conversion.

param[5] in InputFunctionCallSafe's signature is Node *, but the above
code is assigning it an LLVMValueRef for iocoerce's escontext whose
type is ErrorSaveContext.

Maybe some other param is wrong.

I tried various ways to fix both, but with no success.  My way of
checking for failure is to disassemble the IR code in .bc files
(generated with jit_dump_bitcode) with llvm-dis and finding that it
gives me errors such as:

$ llvm-dis 58536.0.bc
llvm-dis: error: Invalid record (Producer: 'LLVM7.0.1' Reader: 'LLVM 7.0.1')

$ llvm-dis 58536.0.bc
llvm-dis: error: Invalid cast (Producer: 'LLVM7.0.1' Reader: 'LLVM 7.0.1')


--
Thanks, Amit Langote
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-04 13:26  Amit Langote <amitlangote09@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-10-04 13:26 UTC (permalink / raw)
  To: Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers; jian he <jian.universality@gmail.com>

On Tue, Oct 3, 2023 at 10:11 PM Amit Langote <amitlangote09@gmail.com> wrote:
> On Mon, Oct 2, 2023 at 2:26 PM Amit Langote <amitlangote09@gmail.com> wrote:
> > On Mon, Oct 2, 2023 at 1:24 PM Amit Langote <amitlangote09@gmail.com> wrote:
> > > Pushed this 30 min ago (no email on -committers yet!) and am looking
> > > at the following llvm crash reported by buildfarm animal pogona [1]:
> > >
> > > #4  0x00007f5bceb673d5 in __assert_fail_base (fmt=0x7f5bcecdbdc8
> > > "%s%s%s:%u: %s%sAssertion `%s' failed.\\n%n",
> > > assertion=assertion@entry=0x7f5bc1336419 "(i >= FTy->getNumParams() ||
> > > FTy->getParamType(i) == Args[i]->getType()) && \\"Calling a function
> > > with a bad signature!\\"", file=file@entry=0x7f5bc1336051
> > > "/home/bf/src/llvm-project-5/llvm/lib/IR/Instructions.cpp",
> > > line=line@entry=299, function=function@entry=0x7f5bc13362af "void
> > > llvm::CallInst::init(llvm::FunctionType *, llvm::Value *,
> > > ArrayRef<llvm::Value *>, ArrayRef<llvm::OperandBundleDef>, const
> > > llvm::Twine &)") at ./assert/assert.c:92
> > > #5  0x00007f5bceb763a2 in __assert_fail (assertion=0x7f5bc1336419 "(i
> > > >= FTy->getNumParams() || FTy->getParamType(i) == Args[i]->getType())
> > > && \\"Calling a function with a bad signature!\\"",
> > > file=0x7f5bc1336051
> > > "/home/bf/src/llvm-project-5/llvm/lib/IR/Instructions.cpp", line=299,
> > > function=0x7f5bc13362af "void llvm::CallInst::init(llvm::FunctionType
> > > *, llvm::Value *, ArrayRef<llvm::Value *>,
> > > ArrayRef<llvm::OperandBundleDef>, const llvm::Twine &)") at
> > > ./assert/assert.c:101
> > > #6  0x00007f5bc110f138 in llvm::CallInst::init (this=0x557a91f3e508,
> > > FTy=0x557a91ed9ae0, Func=0x557a91f8be88, Args=..., Bundles=...,
> > > NameStr=...) at
> > > /home/bf/src/llvm-project-5/llvm/lib/IR/Instructions.cpp:297
> > > #7  0x00007f5bc0fa579d in llvm::CallInst::CallInst
> > > (this=0x557a91f3e508, Ty=0x557a91ed9ae0, Func=0x557a91f8be88,
> > > Args=..., Bundles=..., NameStr=..., InsertBefore=0x0) at
> > > /home/bf/src/llvm-project-5/llvm/include/llvm/IR/Instructions.h:1934
> > > #8  0x00007f5bc0fa538c in llvm::CallInst::Create (Ty=0x557a91ed9ae0,
> > > Func=0x557a91f8be88, Args=..., Bundles=..., NameStr=...,
> > > InsertBefore=0x0) at
> > > /home/bf/src/llvm-project-5/llvm/include/llvm/IR/Instructions.h:1444
> > > #9  0x00007f5bc0fa51f9 in llvm::IRBuilder<llvm::ConstantFolder,
> > > llvm::IRBuilderDefaultInserter>::CreateCall (this=0x557a91f9c6a0,
> > > FTy=0x557a91ed9ae0, Callee=0x557a91f8be88, Args=..., Name=...,
> > > FPMathTag=0x0) at
> > > /home/bf/src/llvm-project-5/llvm/include/llvm/IR/IRBuilder.h:1669
> > > #10 0x00007f5bc100edda in llvm::IRBuilder<llvm::ConstantFolder,
> > > llvm::IRBuilderDefaultInserter>::CreateCall (this=0x557a91f9c6a0,
> > > Callee=0x557a91f8be88, Args=..., Name=..., FPMathTag=0x0) at
> > > /home/bf/src/llvm-project-5/llvm/include/llvm/IR/IRBuilder.h:1663
> > > #11 0x00007f5bc100714e in LLVMBuildCall (B=0x557a91f9c6a0,
> > > Fn=0x557a91f8be88, Args=0x7ffde6fa0b50, NumArgs=6, Name=0x7f5bc30b648c
> > > "funccall_iocoerce_in_safe") at
> > > /home/bf/src/llvm-project-5/llvm/lib/IR/Core.cpp:2964
> > > #12 0x00007f5bc30af861 in llvm_compile_expr (state=0x557a91fbeac0) at
> > > /home/bf/bf-build/pogona/HEAD/pgsql.build/../pgsql/src/backend/jit/llvm/llvmjit_expr.c:1373
> > >
> > > This seems to me to be complaining about the following addition:
> > >
> > > +                   {
> > > +                       Oid         ioparam = op->d.iocoerce.typioparam;
> > > +                       LLVMValueRef v_params[6];
> > > +                       LLVMValueRef v_success;
> > > +
> > > +                       v_params[0] = l_ptr_const(op->d.iocoerce.finfo_in,
> > > +                                                 l_ptr(StructFmgrInfo));
> > > +                       v_params[1] = v_output;
> > > +                       v_params[2] = l_oid_const(lc, ioparam);
> > > +                       v_params[3] = l_int32_const(lc, -1);
> > > +                       v_params[4] = l_ptr_const(op->d.iocoerce.escontext,
> > > +
> > > l_ptr(StructErrorSaveContext));
> > >
> > > -                   LLVMBuildStore(b, v_retval, v_resvaluep);
> > > +                       /*
> > > +                        * InputFunctionCallSafe() will write directly into
> > > +                        * *op->resvalue.
> > > +                        */
> > > +                       v_params[5] = v_resvaluep;
> > > +
> > > +                       v_success = LLVMBuildCall(b, llvm_pg_func(mod,
> > > "InputFunctionCallSafe"),
> > > +                                                 v_params, lengthof(v_params),
> > > +                                                 "funccall_iocoerce_in_safe");
> > > +
> > > +                       /*
> > > +                        * Return null if InputFunctionCallSafe() encountered
> > > +                        * an error.
> > > +                        */
> > > +                       v_resnullp = LLVMBuildICmp(b, LLVMIntEQ, v_success,
> > > +                                                  l_sbool_const(0), "");
> > > +                   }
> >
> > Although most animals except pogona looked fine, I've decided to revert the patch for now.
> >
> > IIUC, LLVM is complaining that the code in the above block is not passing the arguments of InputFunctionCallSafe() using the correct types.  I'm not exactly sure which particular argument is not handled correctly in the above code, but perhaps it's:
> >
> >
> > +                       v_params[1] = v_output;
> >
> > which maps to char *str argument of InputFunctionCallSafe().  v_output is set in the code preceding the above block as follows:
> >
> >                     /* and call output function (can never return NULL) */
> >                     v_output = LLVMBuildCall(b, v_fn_out, &v_fcinfo_out,
> >                                              1, "funccall_coerce_out");
> >
> > I thought that it would be fine to pass it as-is to the call of InputFunctionCallSafe() given that v_fn_out is a call to a function that returns char *, but perhaps not.
>
> OK, I think I could use some help from LLVM experts here.
>
> So, the LLVM code involving setting up a call to
> InputFunctionCallSafe() seems to *work*, but BF animal pogona's debug
> build (?) is complaining that the parameter types don't match up.
> Parameters are set up as follows:
>
> +                   {
> +                       Oid         ioparam = op->d.iocoerce.typioparam;
> +                       LLVMValueRef v_params[6];
> +                       LLVMValueRef v_success;
> +
> +                       v_params[0] = l_ptr_const(op->d.iocoerce.finfo_in,
> +                                                 l_ptr(StructFmgrInfo));
> +                       v_params[1] = v_output;
> +                       v_params[2] = l_oid_const(lc, ioparam);
> +                       v_params[3] = l_int32_const(lc, -1);
> +                       v_params[4] = l_ptr_const(op->d.iocoerce.escontext,
> +
> l_ptr(StructErrorSaveContext));
>  +                       /*
> +                        * InputFunctionCallSafe() will write directly into
> +                        * *op->resvalue.
> +                        */
> +                       v_params[5] = v_resvaluep;
> +
> +                       v_success = LLVMBuildCall(b, llvm_pg_func(mod,
> "InputFunctionCallSafe"),
> +                                                 v_params, lengthof(v_params),
> +                                                 "funccall_iocoerce_in_safe");
> +
> +                       /*
> +                        * Return null if InputFunctionCallSafe() encountered
> +                        * an error.
> +                        */
> +                       v_resnullp = LLVMBuildICmp(b, LLVMIntEQ, v_success,
> +                                                  l_sbool_const(0), "");
> +                   }
>
> And here's InputFunctionCallSafe's signature:
>
> bool
> InputFunctionCallSafe(FmgrInfo *flinfo, Datum d,
>                       Oid typioparam, int32 typmod,
>                       fmNodePtr escontext,
>                       Datum *result)
>
> I suspected that assignment to either param[1] or param[5] might be wrong.
>
> param[1] in InputFunctionCallSafe's signature is char *, but the code
> assigns it v_output, which is an LLVMValueRef for the output
> function's output, a Datum, so I thought LLVM's type checker is
> complaining that I'm trying to pass the Datum to char * without
> appropriate conversion.
>
> param[5] in InputFunctionCallSafe's signature is Node *, but the above
> code is assigning it an LLVMValueRef for iocoerce's escontext whose
> type is ErrorSaveContext.
>
> Maybe some other param is wrong.
>
> I tried various ways to fix both, but with no success.  My way of
> checking for failure is to disassemble the IR code in .bc files
> (generated with jit_dump_bitcode) with llvm-dis and finding that it
> gives me errors such as:
>
> $ llvm-dis 58536.0.bc
> llvm-dis: error: Invalid record (Producer: 'LLVM7.0.1' Reader: 'LLVM 7.0.1')
>
> $ llvm-dis 58536.0.bc
> llvm-dis: error: Invalid cast (Producer: 'LLVM7.0.1' Reader: 'LLVM 7.0.1')

So I built LLVM sources to get asserts like pogona:

$ llvm-config --version
15.0.7
$ llvm-config --assertion-mode
ON

and I do now get a crash with bt that looks like this (not same as pogona):

#0  0x00007fe31e83c387 in raise () from /lib64/libc.so.6
#1  0x00007fe31e83da78 in abort () from /lib64/libc.so.6
#2  0x00007fe31e8351a6 in __assert_fail_base () from /lib64/libc.so.6
#3  0x00007fe31e835252 in __assert_fail () from /lib64/libc.so.6
#4  0x00007fe3136d8132 in llvm::CallInst::init(llvm::FunctionType*,
llvm::Value*, llvm::ArrayRef<llvm::Value*>,
llvm::ArrayRef<llvm::OperandBundleDefT<llvm::Value*> >, llvm::Twine
const&) ()
   from /home/amit/llvm/lib/libLLVMCore.so.15
#5  0x00007fe31362137a in
llvm::IRBuilderBase::CreateCall(llvm::FunctionType*, llvm::Value*,
llvm::ArrayRef<llvm::Value*>, llvm::Twine const&, llvm::MDNode*) ()
from /home/amit/llvm/lib/libLLVMCore.so.15
#6  0x00007fe31362d627 in LLVMBuildCall () from
/home/amit/llvm/lib/libLLVMCore.so.15
#7  0x00007fe3205e7e92 in llvm_compile_expr (state=0x1114e48) at
llvmjit_expr.c:1374
#8  0x0000000000bd3fbc in jit_compile_expr (state=0x1114e48) at jit.c:177
#9  0x000000000072442b in ExecReadyExpr (state=0x1114e48) at execExpr.c:880
#10 0x000000000072387c in ExecBuildProjectionInfo
(targetList=0x1110840, econtext=0x1114a20, slot=0x1114db0,
    parent=0x1114830, inputDesc=0x1114ab0) at execExpr.c:484
#11 0x000000000074e917 in ExecAssignProjectionInfo
(planstate=0x1114830, inputDesc=0x1114ab0) at execUtils.c:547
#12 0x000000000074ea02 in ExecConditionalAssignProjectionInfo
(planstate=0x1114830, inputDesc=0x1114ab0, varno=2)
    at execUtils.c:585
#13 0x0000000000749814 in ExecAssignScanProjectionInfo
(node=0x1114830) at execScan.c:276
#14 0x0000000000790bf0 in ExecInitValuesScan (node=0x1045020,
estate=0x1114600, eflags=32)
    at nodeValuesscan.c:257
#15 0x00000000007451c9 in ExecInitNode (node=0x1045020,
estate=0x1114600, eflags=32) at execProcnode.c:265
#16 0x000000000073a952 in InitPlan (queryDesc=0x1070760, eflags=32) at
execMain.c:968
#17 0x0000000000739828 in standard_ExecutorStart (queryDesc=0x1070760,
eflags=32) at execMain.c:266
#18 0x000000000073959d in ExecutorStart (queryDesc=0x1070760,
eflags=0) at execMain.c:145
#19 0x00000000009c1aaa in PortalStart (portal=0x10bf7d0, params=0x0,
eflags=0, snapshot=0x0) at pquery.c:517
#20 0x00000000009bbba8 in exec_simple_query (
    query_string=0x10433c0 "select i::pg_lsn from (values ('x/a'),
('b/b')) a(i);") at postgres.c:1233
#21 0x00000000009c0263 in PostgresMain (dbname=0x1079750 "postgres",
username=0x1079738 "amit")
    at postgres.c:4652
#22 0x00000000008f72d6 in BackendRun (port=0x106e740) at postmaster.c:4439
#23 0x00000000008f6c6f in BackendStartup (port=0x106e740) at postmaster.c:4167
#24 0x00000000008f363e in ServerLoop () at postmaster.c:1781
#25 0x00000000008f300e in PostmasterMain (argc=5, argv=0x103dc60) at
postmaster.c:1465
#26 0x00000000007bbfb4 in main (argc=5, argv=0x103dc60) at main.c:198

The LLVMBuildCall() in frame #6 is added by the patch that I also
mentioned in the previous replies.  I haven't yet pinpointed down
which of the LLVM's asserts it is, nor have I been able to walk
through LLVM source code using gdb to figure what the new code is
doing wrong.  Maybe I'm still missing a trick or two...

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-06 09:23  Amit Langote <amitlangote09@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-10-06 09:23 UTC (permalink / raw)
  To: Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers; jian he <jian.universality@gmail.com>

On Wed, Oct 4, 2023 at 10:26 PM Amit Langote <amitlangote09@gmail.com> wrote:
> On Tue, Oct 3, 2023 at 10:11 PM Amit Langote <amitlangote09@gmail.com> wrote:
> > On Mon, Oct 2, 2023 at 2:26 PM Amit Langote <amitlangote09@gmail.com> wrote:
> > > On Mon, Oct 2, 2023 at 1:24 PM Amit Langote <amitlangote09@gmail.com> wrote:
> > > > Pushed this 30 min ago (no email on -committers yet!) and am looking
> > > > at the following llvm crash reported by buildfarm animal pogona [1]:
> > > > This seems to me to be complaining about the following addition:
> > > >
> > > > +                   {
> > > > +                       Oid         ioparam = op->d.iocoerce.typioparam;
> > > > +                       LLVMValueRef v_params[6];
> > > > +                       LLVMValueRef v_success;
> > > > +
> > > > +                       v_params[0] = l_ptr_const(op->d.iocoerce.finfo_in,
> > > > +                                                 l_ptr(StructFmgrInfo));
> > > > +                       v_params[1] = v_output;
> > > > +                       v_params[2] = l_oid_const(lc, ioparam);
> > > > +                       v_params[3] = l_int32_const(lc, -1);
> > > > +                       v_params[4] = l_ptr_const(op->d.iocoerce.escontext,
> > > > +
> > > > l_ptr(StructErrorSaveContext));
> > > >
> > > > -                   LLVMBuildStore(b, v_retval, v_resvaluep);
> > > > +                       /*
> > > > +                        * InputFunctionCallSafe() will write directly into
> > > > +                        * *op->resvalue.
> > > > +                        */
> > > > +                       v_params[5] = v_resvaluep;
> > > > +
> > > > +                       v_success = LLVMBuildCall(b, llvm_pg_func(mod,
> > > > "InputFunctionCallSafe"),
> > > > +                                                 v_params, lengthof(v_params),
> > > > +                                                 "funccall_iocoerce_in_safe");
> > > > +
> > > > +                       /*
> > > > +                        * Return null if InputFunctionCallSafe() encountered
> > > > +                        * an error.
> > > > +                        */
> > > > +                       v_resnullp = LLVMBuildICmp(b, LLVMIntEQ, v_success,
> > > > +                                                  l_sbool_const(0), "");
> > > > +                   }
> > >
> ...I haven't yet pinpointed down
> which of the LLVM's asserts it is, nor have I been able to walk
> through LLVM source code using gdb to figure what the new code is
> doing wrong.  Maybe I'm still missing a trick or two...

I finally managed to analyze the crash by getting the correct LLVM build.

So the following bits are the culprits:

1. v_output needed to be converted from being reference to a Datum to
be reference to char * as follows before passing to
InputFunctionCallSafe():

-                       v_params[1] = v_output;
+                       v_params[1] = LLVMBuildIntToPtr(b, v_output,
+
l_ptr(LLVMInt8TypeInContext(lc)),
+                                                       "");

2. Assignment of op->d.iocoerce.escontext needed to be changed like this:

                        v_params[4] = l_ptr_const(op->d.iocoerce.escontext,
-
l_ptr(StructErrorSaveContext));
+                                                 l_ptr(StructNode));

3. v_success needed to be "zero-extended" to match in type with
whatever s_bool_const() produces, as follows:

+                       v_success = LLVMBuildZExt(b, v_success,
TypeStorageBool, "");
                        v_resnullp = LLVMBuildICmp(b, LLVMIntEQ, v_success,
                                                   l_sbool_const(0), "");

No more crashes with the above fixes.

Attached shows the delta against the patch I reverted.  I'll push the
fixed up version on Monday.

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

Attachments:

iocoerce-llvm-fixes.diff (application/octet-stream, 3.0K) download diff --git a/src/backend/jit/llvm/llvmjit.c b/src/backend/jit/llvm/llvmjit.c index 125e1e73ae..5a9be73957 100644 --- a/src/backend/jit/llvm/llvmjit.c +++ b/src/backend/jit/llvm/llvmjit.c @@ -77,7 +77,7 @@ LLVMTypeRef StructExprState; LLVMTypeRef StructAggState; LLVMTypeRef StructAggStatePerGroupData; LLVMTypeRef StructAggStatePerTransData; -LLVMTypeRef StructErrorSaveContext; +LLVMTypeRef StructNode; LLVMValueRef AttributeTemplate; @@ -1130,7 +1130,7 @@ llvm_create_types(void) StructAggState = llvm_pg_var_type("StructAggState"); StructAggStatePerGroupData = llvm_pg_var_type("StructAggStatePerGroupData"); StructAggStatePerTransData = llvm_pg_var_type("StructAggStatePerTransData"); - StructErrorSaveContext = llvm_pg_var_type("StructErrorSaveContext"); + StructNode = llvm_pg_var_type("StructNode"); AttributeTemplate = LLVMGetNamedFunction(llvm_types_module, "AttributeTemplate"); } diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 7d44a4c9f4..aa0e09bd7a 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -1358,11 +1358,13 @@ llvm_compile_expr(ExprState *state) v_params[0] = l_ptr_const(op->d.iocoerce.finfo_in, l_ptr(StructFmgrInfo)); - v_params[1] = v_output; + v_params[1] = LLVMBuildIntToPtr(b, v_output, + l_ptr(LLVMInt8TypeInContext(lc)), + ""); v_params[2] = l_oid_const(lc, ioparam); v_params[3] = l_int32_const(lc, -1); v_params[4] = l_ptr_const(op->d.iocoerce.escontext, - l_ptr(StructErrorSaveContext)); + l_ptr(StructNode)); /* * InputFunctionCallSafe() will write directly into @@ -1378,6 +1380,7 @@ llvm_compile_expr(ExprState *state) * Return null if InputFunctionCallSafe() encountered * an error. */ + v_success = LLVMBuildZExt(b, v_success, TypeStorageBool, ""); v_resnullp = LLVMBuildICmp(b, LLVMIntEQ, v_success, l_sbool_const(0), ""); } diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c index e1e9625038..78f1f14ba0 100644 --- a/src/backend/jit/llvm/llvmjit_types.c +++ b/src/backend/jit/llvm/llvmjit_types.c @@ -67,7 +67,7 @@ TupleTableSlot StructTupleTableSlot; HeapTupleTableSlot StructHeapTupleTableSlot; MinimalTupleTableSlot StructMinimalTupleTableSlot; TupleDescData StructTupleDescData; -ErrorSaveContext StructErrorSaveContext; +Node StructNode; /* diff --git a/src/include/jit/llvmjit.h b/src/include/jit/llvmjit.h index 5b7681eba9..e4ff3be566 100644 --- a/src/include/jit/llvmjit.h +++ b/src/include/jit/llvmjit.h @@ -83,7 +83,7 @@ extern PGDLLIMPORT LLVMTypeRef StructExprState; extern PGDLLIMPORT LLVMTypeRef StructAggState; extern PGDLLIMPORT LLVMTypeRef StructAggStatePerTransData; extern PGDLLIMPORT LLVMTypeRef StructAggStatePerGroupData; -extern PGDLLIMPORT LLVMTypeRef StructErrorSaveContext; +extern PGDLLIMPORT LLVMTypeRef StructNode; extern PGDLLIMPORT LLVMValueRef AttributeTemplate;
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-06 10:01  Alvaro Herrera <alvherre@alvh.no-ip.org>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Alvaro Herrera @ 2023-10-06 10:01 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers; jian he <jian.universality@gmail.com>

On 2023-Oct-06, Amit Langote wrote:

> 2. Assignment of op->d.iocoerce.escontext needed to be changed like this:
> 
>                         v_params[4] = l_ptr_const(op->d.iocoerce.escontext,
> -
> l_ptr(StructErrorSaveContext));
> +                                                 l_ptr(StructNode));

Oh, so you had to go back to using StructNode in order to get this
fixed?  That's weird.  Is it just because InputFunctionCallSafe is
defined to take fmNodePtr?  (I still fail to see that a pointer to
ErrorSaveContext would differ in any material way from a pointer to
Node).


Another think I thought was weird is that it would only crash in LLVM5
debug and not the other LLVM-enabled animals, but looking closer at the
buildfarm results, I think that may have been only because you reverted
too quickly, and phycodorus and petalura didn't actually run with
7fbc75b26ed8 before you reverted it.  Dragonet did make a run with it,
but it's marked as "LLVM optimized" instead of "LLVM debug".  I suppose
that must be making a difference.

-- 
Álvaro Herrera        Breisgau, Deutschland  —  https://www.EnterpriseDB.com/
"World domination is proceeding according to plan"        (Andrew Morton)





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-06 11:27  Amit Langote <amitlangote09@gmail.com>
  parent: Alvaro Herrera <alvherre@alvh.no-ip.org>
  0 siblings, 0 replies; 300+ messages in thread

From: Amit Langote @ 2023-10-06 11:27 UTC (permalink / raw)
  To: Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers; jian he <jian.universality@gmail.com>

On Fri, Oct 6, 2023 at 19:01 Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:

> On 2023-Oct-06, Amit Langote wrote:
>
> > 2. Assignment of op->d.iocoerce.escontext needed to be changed like this:
> >
> >                         v_params[4] =
> l_ptr_const(op->d.iocoerce.escontext,
> > -
> > l_ptr(StructErrorSaveContext));
> > +                                                 l_ptr(StructNode));
>
> Oh, so you had to go back to using StructNode in order to get this
> fixed?  That's weird.  Is it just because InputFunctionCallSafe is
> defined to take fmNodePtr?  (I still fail to see that a pointer to
> ErrorSaveContext would differ in any material way from a pointer to
> Node).


The difference matters to LLVM’s type system, which considers Node to be a
type with 1 sub-type (struct member) and ErrorSaveContext with 4 sub-types.
It doesn’t seem to understand that both share the first member.


Another think I thought was weird is that it would only crash in LLVM5
> debug and not the other LLVM-enabled animals, but looking closer at the
> buildfarm results, I think that may have been only because you reverted
> too quickly, and phycodorus and petalura didn't actually run with
> 7fbc75b26ed8 before you reverted it.  Dragonet did make a run with it,
> but it's marked as "LLVM optimized" instead of "LLVM debug".  I suppose
> that must be making a difference.


AFAICS, only assert-enabled LLVM builds crash.

>

^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-06 21:48  Andres Freund <andres@anarazel.de>
  parent: Amit Langote <amitlangote09@gmail.com>
  1 sibling, 3 replies; 300+ messages in thread

From: Andres Freund @ 2023-10-06 21:48 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; jian he <jian.universality@gmail.com>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

Hi,

On 2023-09-29 13:57:46 +0900, Amit Langote wrote:
> Thanks.  I will push the attached 0001 shortly.

Sorry for not looking at this earlier.

Have you done benchmarking to verify that 0001 does not cause performance
regressions? I'd not be suprised if it did. I'd split the soft-error path into
a separate opcode. For JIT it can largely be implemented using the same code,
eliding the check if it's the non-soft path. Or you can just put it into an
out-of-line function.

I don't like adding more stuff to ExprState. This one seems particularly
awkward, because it might be used by more than one level in an expression
subtree, which means you really need to save/restore old values when
recursing.


> @@ -1579,25 +1582,13 @@ ExecInitExprRec(Expr *node, ExprState *state,
>
>  				/* lookup the result type's input function */
>  				scratch.d.iocoerce.finfo_in = palloc0(sizeof(FmgrInfo));
> -				scratch.d.iocoerce.fcinfo_data_in = palloc0(SizeForFunctionCallInfo(3));
> -
>  				getTypeInputInfo(iocoerce->resulttype,
> -								 &iofunc, &typioparam);
> +								 &iofunc, &scratch.d.iocoerce.typioparam);
>  				fmgr_info(iofunc, scratch.d.iocoerce.finfo_in);
>  				fmgr_info_set_expr((Node *) node, scratch.d.iocoerce.finfo_in);
> -				InitFunctionCallInfoData(*scratch.d.iocoerce.fcinfo_data_in,
> -										 scratch.d.iocoerce.finfo_in,
> -										 3, InvalidOid, NULL, NULL);
>
> -				/*
> -				 * We can preload the second and third arguments for the input
> -				 * function, since they're constants.
> -				 */
> -				fcinfo_in = scratch.d.iocoerce.fcinfo_data_in;
> -				fcinfo_in->args[1].value = ObjectIdGetDatum(typioparam);
> -				fcinfo_in->args[1].isnull = false;
> -				fcinfo_in->args[2].value = Int32GetDatum(-1);
> -				fcinfo_in->args[2].isnull = false;
> +				/* Use the ErrorSaveContext passed by the caller. */
> +				scratch.d.iocoerce.escontext = state->escontext;
>
>  				ExprEvalPushStep(state, &scratch);
>  				break;

I think it's likely that removing the optimization of not needing to set these
arguments ahead of time will result in a performance regression. Not to speak
of initializing the fcinfo from scratch on every evaluation of the expression.

I think this shouldn't not be merged as is.



>  src/backend/parser/gram.y                   |  348 +++++-

This causes a nontrivial increase in the size of the parser (~5% in an
optimized build here), I wonder if we can do better.


> +/*
> + * Push steps to evaluate a JsonExpr and its various subsidiary expressions.
> + */
> +static void
> +ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state,
> +				 Datum *resv, bool *resnull,
> +				 ExprEvalStep *scratch)
> +{
> +	JsonExprState *jsestate = palloc0(sizeof(JsonExprState));
> +	JsonExprPreEvalState *pre_eval = &jsestate->pre_eval;
> +	ListCell   *argexprlc;
> +	ListCell   *argnamelc;
> +	int			skip_step_off = -1;
> +	int			passing_args_step_off = -1;
> +	int			coercion_step_off = -1;
> +	int			coercion_finish_step_off = -1;
> +	int			behavior_step_off = -1;
> +	int			onempty_expr_step_off = -1;
> +	int			onempty_jump_step_off = -1;
> +	int			onerror_expr_step_off = -1;
> +	int			onerror_jump_step_off = -1;
> +	int			result_coercion_jump_step_off = -1;
> +	List	   *adjust_jumps = NIL;
> +	ListCell   *lc;
> +	ExprEvalStep *as;
> +
> +	jsestate->jsexpr = jexpr;
> +
> +	/*
> +	 * Add steps to compute formatted_expr, pathspec, and PASSING arg
> +	 * expressions as things that must be evaluated *before* the actual JSON
> +	 * path expression.
> +	 */
> +	ExecInitExprRec((Expr *) jexpr->formatted_expr, state,
> +					&pre_eval->formatted_expr.value,
> +					&pre_eval->formatted_expr.isnull);
> +	ExecInitExprRec((Expr *) jexpr->path_spec, state,
> +					&pre_eval->pathspec.value,
> +					&pre_eval->pathspec.isnull);
> +
> +	/*
> +	 * Before pushing steps for PASSING args, push a step to decide whether to
> +	 * skip evaluating the args and the JSON path expression depending on
> +	 * whether either of formatted_expr and pathspec is NULL; see
> +	 * ExecEvalJsonExprSkip().
> +	 */
> +	scratch->opcode = EEOP_JSONEXPR_SKIP;
> +	scratch->d.jsonexpr_skip.jsestate = jsestate;
> +	skip_step_off = state->steps_len;
> +	ExprEvalPushStep(state, scratch);

Could SKIP be implemented using EEOP_JUMP_IF_NULL with a bit of work?  I see
that it sets jsestate->post_eval.jcstate, but I don't understand why it needs
to be done that way. /* ExecEvalJsonExprCoercion() depends on this. */ doesn't
explain that much.


> +	/* PASSING args. */
> +	jsestate->pre_eval.args = NIL;
> +	passing_args_step_off = state->steps_len;
> +	forboth(argexprlc, jexpr->passing_values,
> +			argnamelc, jexpr->passing_names)
> +	{
> +		Expr	   *argexpr = (Expr *) lfirst(argexprlc);
> +		String	   *argname = lfirst_node(String, argnamelc);
> +		JsonPathVariable *var = palloc(sizeof(*var));
> +
> +		var->name = pstrdup(argname->sval);

Why does this need to be strdup'd?


> +	/* Step for the actual JSON path evaluation; see ExecEvalJsonExpr(). */
> +	scratch->opcode = EEOP_JSONEXPR_PATH;
> +	scratch->d.jsonexpr.jsestate = jsestate;
> +	ExprEvalPushStep(state, scratch);
> +
> +	/*
> +	 * Step to handle ON ERROR and ON EMPTY behavior.  Also, to handle errors
> +	 * that may occur during coercion handling.
> +	 *
> +	 * See ExecEvalJsonExprBehavior().
> +	 */
> +	scratch->opcode = EEOP_JSONEXPR_BEHAVIOR;
> +	scratch->d.jsonexpr_behavior.jsestate = jsestate;
> +	behavior_step_off = state->steps_len;
> +	ExprEvalPushStep(state, scratch);

From what I can tell there a) can never be a step between EEOP_JSONEXPR_PATH
and EEOP_JSONEXPR_BEHAVIOR b) EEOP_JSONEXPR_PATH ends with an unconditional
branch. What's the point of the two different steps here?




>
> +		EEO_CASE(EEOP_JSONEXPR_PATH)
> +		{
> +			/* too complex for an inline implementation */
> +			ExecEvalJsonExpr(state, op, econtext);
> +			EEO_NEXT();
> +		}

Why does EEOP_JSONEXPR_PATH call ExecEvalJsonExpr, the names don't match...


> +		EEO_CASE(EEOP_JSONEXPR_SKIP)
> +		{
> +			/* too complex for an inline implementation */
> +			EEO_JUMP(ExecEvalJsonExprSkip(state, op));
> +		}
...


> +		EEO_CASE(EEOP_JSONEXPR_COERCION_FINISH)
> +		{
> +			/* too complex for an inline implementation */
> +			EEO_JUMP(ExecEvalJsonExprCoercionFinish(state, op));
> +		}

This seems to just return op->d.jsonexpr_coercion_finish.jump_coercion_error
or op->d.jsonexpr_coercion_finish.jump_coercion_done. Which makes me think
it'd be better to return a boolean? Particularly because that's how you
already implemented it for JIT (except that you did it by hardcoding the jump
step to compare to, which seems odd).


Separately, why do we even need a jump for both cases, and not just for the
error case?


> +		EEO_CASE(EEOP_JSONEXPR_BEHAVIOR)
> +		{
> +			/* too complex for an inline implementation */
> +			EEO_JUMP(ExecEvalJsonExprBehavior(state, op));
> +		}
> +
> +		EEO_CASE(EEOP_JSONEXPR_COERCION)
> +		{
> +			/* too complex for an inline implementation */
> +			EEO_JUMP(ExecEvalJsonExprCoercion(state, op, econtext,
> +											  *op->resvalue, *op->resnull));
> +		}

I wonder if this is the right design for this op - you're declaring this to be
op not worth implementing inline, yet you then have it implemented by hand for JIT.


> +/*
> + * Evaluate given JsonExpr by performing the specified JSON operation.
> + *
> + * This also populates the JsonExprPostEvalState with the information needed
> + * by the subsequent steps that handle the specified JsonBehavior.
> + */
> +void
> +ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
> +{
> +	JsonExprState *jsestate = op->d.jsonexpr.jsestate;
> +	JsonExprPreEvalState *pre_eval = &jsestate->pre_eval;
> +	JsonExprPostEvalState *post_eval = &jsestate->post_eval;
> +	JsonExpr   *jexpr = jsestate->jsexpr;
> +	Datum		item;
> +	Datum		res = (Datum) 0;
> +	bool		resnull = true;
> +	JsonPath   *path;
> +	bool		throw_error = (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR);
> +	bool	   *error = &post_eval->error;
> +	bool	   *empty = &post_eval->empty;
> +
> +	item = pre_eval->formatted_expr.value;
> +	path = DatumGetJsonPathP(pre_eval->pathspec.value);
> +
> +	/* Reset JsonExprPostEvalState for this evaluation. */
> +	memset(post_eval, 0, sizeof(*post_eval));
> +
> +	switch (jexpr->op)
> +	{
> +		case JSON_EXISTS_OP:
> +			{
> +				bool		exists = JsonPathExists(item, path,
> +													!throw_error ? error : NULL,
> +													pre_eval->args);
> +
> +				post_eval->jcstate = jsestate->result_jcstate;
> +				if (*error)
> +				{
> +					*op->resnull = true;
> +					*op->resvalue = (Datum) 0;
> +					return;
> +				}
> +
> +				resnull = false;
> +				res = BoolGetDatum(exists);
> +				break;
> +			}

Kinda seems there should be a EEOP_JSON_EXISTS/JSON_QUERY_OP op, instead of
implementing it all inside ExecEvalJsonExpr. I think this might obsolete
needing to rediscover that the value is null in SKIP etc?


> +		case JSON_QUERY_OP:
> +			res = JsonPathQuery(item, path, jexpr->wrapper, empty,
> +								!throw_error ? error : NULL,
> +								pre_eval->args);
> +
> +			post_eval->jcstate = jsestate->result_jcstate;
> +			if (*error)
> +			{
> +				*op->resnull = true;
> +				*op->resvalue = (Datum) 0;
> +				return;
> +			}
> +			resnull = !DatumGetPointer(res);

Shoulnd't this check empty?

FWIW, it's also pretty odd that JsonPathQuery() once
		return (Datum) 0;
and later does
	return PointerGetDatum(NULL);


> +		case JSON_VALUE_OP:
> +			{
> +				JsonbValue *jbv = JsonPathValue(item, path, empty,
> +												!throw_error ? error : NULL,
> +												pre_eval->args);
> +
> +				/* Might get overridden below by an item_jcstate. */
> +				post_eval->jcstate = jsestate->result_jcstate;
> +				if (*error)
> +				{
> +					*op->resnull = true;
> +					*op->resvalue = (Datum) 0;
> +					return;
> +				}
> +
> +				if (!jbv)		/* NULL or empty */
> +				{
> +					resnull = true;
> +					break;
> +				}
> +
> +				Assert(!*empty);
> +
> +				resnull = false;
> +
> +				/* Coerce scalar item to the output type */
> +
> +				/*
> +				 * If the requested output type is json(b), use
> +				 * JsonExprState.result_coercion to do the coercion.
> +				 */
> +				if (jexpr->returning->typid == JSONOID ||
> +					jexpr->returning->typid == JSONBOID)
> +				{
> +					/* Use result_coercion from json[b] to the output type */
> +					res = JsonbPGetDatum(JsonbValueToJsonb(jbv));
> +					break;
> +				}
> +
> +				/*
> +				 * Else, use one of the item_coercions.
> +				 *
> +				 * Error out if no cast exists to coerce SQL/JSON item to the
> +				 * the output type.
> +				 */
> +				res = ExecPrepareJsonItemCoercion(jbv,
> +												  jsestate->item_jcstates,
> +												  &post_eval->jcstate);
> +				if (post_eval->jcstate &&
> +					post_eval->jcstate->coercion &&
> +					(post_eval->jcstate->coercion->via_io ||
> +					 post_eval->jcstate->coercion->via_populate))
> +				{
> +					if (!throw_error)
> +					{
> +						*op->resnull = true;
> +						*op->resvalue = (Datum) 0;
> +						return;
> +					}
> +
> +					/*
> +					 * Coercion via I/O means here that the cast to the target
> +					 * type simply does not exist.
> +					 */
> +					ereport(ERROR,
> +							(errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE),
> +							 errmsg("SQL/JSON item cannot be cast to target type")));
> +				}
> +				break;
> +			}
> +
> +		default:
> +			elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op);
> +			*op->resnull = true;
> +			*op->resvalue = (Datum) 0;
> +			return;
> +	}
> +
> +	/*
> +	 * If the ON EMPTY behavior is to cause an error, do so here.  Other
> +	 * behaviors will be handled in ExecEvalJsonExprBehavior().
> +	 */
> +	if (*empty)
> +	{
> +		Assert(jexpr->on_empty);	/* it is not JSON_EXISTS */
> +
> +		if (jexpr->on_empty->btype == JSON_BEHAVIOR_ERROR)
> +		{
> +			if (!throw_error)
> +			{
> +				*op->resnull = true;
> +				*op->resvalue = (Datum) 0;
> +				return;
> +			}
> +
> +			ereport(ERROR,
> +					(errcode(ERRCODE_NO_SQL_JSON_ITEM),
> +					 errmsg("no SQL/JSON item")));
> +		}
> +	}
> +
> +	*op->resvalue = res;
> +	*op->resnull = resnull;
> +}
> +
> +/*
> + * Skip calling ExecEvalJson() on the given JsonExpr?

I don't think that function exists.


> + * Returns the step address to be performed next.
> + */
> +int
> +ExecEvalJsonExprSkip(ExprState *state, ExprEvalStep *op)
> +{
> +	JsonExprState *jsestate = op->d.jsonexpr_skip.jsestate;
> +
> +	/*
> +	 * Skip if either of the input expressions has turned out to be NULL,
> +	 * though do execute domain checks for NULLs, which are handled by the
> +	 * coercion step.
> +	 */
> +	if (jsestate->pre_eval.formatted_expr.isnull ||
> +		jsestate->pre_eval.pathspec.isnull)
> +	{
> +		*op->resvalue = (Datum) 0;
> +		*op->resnull = true;
> +
> +		/* ExecEvalJsonExprCoercion() depends on this. */
> +		jsestate->post_eval.jcstate = jsestate->result_jcstate;
> +
> +		return op->d.jsonexpr_skip.jump_coercion;
> +	}
> +
> +	/*
> +	 * Go evaluate the PASSING args if any and subsequently JSON path itself.
> +	 */
> +	return op->d.jsonexpr_skip.jump_passing_args;
> +}
> +
> +/*
> + * Returns the step address to perform the JsonBehavior applicable to
> + * the JSON item that resulted from evaluating the given JsonExpr.
> + *
> + * Returns the step address to be performed next.
> + */
> +int
> +ExecEvalJsonExprBehavior(ExprState *state, ExprEvalStep *op)
> +{
> +	JsonExprState *jsestate = op->d.jsonexpr_behavior.jsestate;
> +	JsonExprPostEvalState *post_eval = &jsestate->post_eval;
> +	JsonBehavior *behavior = NULL;
> +	int			jump_to = -1;
> +
> +	if (post_eval->error || post_eval->coercion_error)
> +	{
> +		behavior = jsestate->jsexpr->on_error;
> +		jump_to = op->d.jsonexpr_behavior.jump_onerror_expr;
> +	}
> +	else if (post_eval->empty)
> +	{
> +		behavior = jsestate->jsexpr->on_empty;
> +		jump_to = op->d.jsonexpr_behavior.jump_onempty_expr;
> +	}
> +	else if (!post_eval->coercion_done)
> +	{
> +		/*
> +		 * If no error or the JSON item is not empty, directly go to the
> +		 * coercion step to coerce the item as is.
> +		 */
> +		return op->d.jsonexpr_behavior.jump_coercion;
> +	}
> +
> +	Assert(behavior);
> +
> +	/*
> +	 * Set up for coercion step that will run to coerce a non-default behavior
> +	 * value.  It should use result_coercion, if any.  Errors that may occur
> +	 * should be thrown for JSON ops other than JSON_VALUE_OP.
> +	 */
> +	if (behavior->btype != JSON_BEHAVIOR_DEFAULT)
> +	{
> +		post_eval->jcstate = jsestate->result_jcstate;
> +		post_eval->coercing_behavior_expr = true;
> +	}
> +
> +	Assert(jump_to >= 0);
> +	return jump_to;
> +}
> +
> +/*
> + * Evaluate or return the step address to evaluate a coercion of a JSON item
> + * to the target type.  The former if the coercion must be done right away by
> + * calling the target type's input function, and for some types, by calling
> + * json_populate_type().
> + *
> + * Returns the step address to be performed next.
> + */
> +int
> +ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op,
> +						 ExprContext *econtext,
> +						 Datum res, bool resnull)
> +{
> +	JsonExprState *jsestate = op->d.jsonexpr_coercion.jsestate;
> +	JsonExpr   *jexpr = jsestate->jsexpr;
> +	JsonExprPostEvalState *post_eval = &jsestate->post_eval;
> +	JsonCoercionState *jcstate = post_eval->jcstate;
> +	char	   *val_string = NULL;
> +	bool		omit_quotes = false;
> +
> +	switch (jexpr->op)
> +	{
> +		case JSON_EXISTS_OP:
> +			if (jcstate && jcstate->jump_eval_expr >= 0)
> +				return jcstate->jump_eval_expr;

Shouldn't this be a compile-time check and instead be handled by simply not
emitting a step instead?


> +			/* No coercion needed. */
> +			post_eval->coercion_done = true;
> +			return op->d.jsonexpr_coercion.jump_coercion_done;

Which then means we also don't need to emit anything here, no?


> +/*
> + * Prepare SQL/JSON item coercion to the output type. Returned a datum of the
> + * corresponding SQL type and a pointer to the coercion state.
> + */
> +static Datum
> +ExecPrepareJsonItemCoercion(JsonbValue *item, List *item_jcstates,
> +							JsonCoercionState **p_item_jcstate)

I might have missed it, but if not: The whole way the coercion stuff works
needs a decent comment explaining how things fit together.

What does "item" really mean here?


> +{
> +	JsonCoercionState *item_jcstate;
> +	Datum		res;
> +	JsonbValue	buf;
> +
> +	if (item->type == jbvBinary &&
> +		JsonContainerIsScalar(item->val.binary.data))
> +	{
> +		bool		res PG_USED_FOR_ASSERTS_ONLY;
> +
> +		res = JsonbExtractScalar(item->val.binary.data, &buf);
> +		item = &buf;
> +		Assert(res);
> +	}
> +
> +	/* get coercion state reference and datum of the corresponding SQL type */
> +	switch (item->type)
> +	{
> +		case jbvNull:
> +			item_jcstate = list_nth(item_jcstates, JsonItemTypeNull);

This seems quite odd. We apparently have a fixed-length array, where specific
offsets have specific meanings, yet it's encoded as a list that's then
accessed with constant offsets?


Right now ExecEvalJsonExpr() stores what ExecPrepareJsonItemCoercion() chooses
in post_eval->jcstate. Which the immediately following
ExecEvalJsonExprBehavior() then digs out again. Then there's also control flow
via post_eval->coercing_behavior_expr.  This is ... not nice.


ISTM that jsestate should have an array of jump targets, indexed by
item->type. Which, for llvm IR, you can encode as a switch statement, instead
of doing control flow via JsonExprState/JsonExprPostEvalState.  There's
obviously a bit more needed, but I think something like that should work, and
simplify things a fair bit.



> @@ -15711,6 +15721,192 @@ func_expr_common_subexpr:
>  					n->location = @1;
>  					$$ = (Node *) n;
>  				}
> +			| JSON_QUERY '('
> +				json_api_common_syntax
> +				json_returning_clause_opt
> +				json_wrapper_behavior
> +				json_quotes_clause_opt
> +			')'
> +				{
> +					JsonFuncExpr *n = makeNode(JsonFuncExpr);
> +
> +					n->op = JSON_QUERY_OP;
> +					n->common = (JsonCommon *) $3;
> +					n->output = (JsonOutput *) $4;
> +					n->wrapper = $5;
> +					if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC)
> +						ereport(ERROR,
> +								(errcode(ERRCODE_SYNTAX_ERROR),
> +								 errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"),
> +								 parser_errposition(@6)));
> +					n->quotes = $6;
> +					n->location = @1;
> +					$$ = (Node *) n;
> +				}
> +			| JSON_QUERY '('
> +				json_api_common_syntax
> +				json_returning_clause_opt
> +				json_wrapper_behavior
> +				json_quotes_clause_opt
> +				json_query_behavior ON EMPTY_P
> +			')'
> +				{
> +					JsonFuncExpr *n = makeNode(JsonFuncExpr);
> +
> +					n->op = JSON_QUERY_OP;
> +					n->common = (JsonCommon *) $3;
> +					n->output = (JsonOutput *) $4;
> +					n->wrapper = $5;
> +					if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC)
> +						ereport(ERROR,
> +								(errcode(ERRCODE_SYNTAX_ERROR),
> +								 errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"),
> +								 parser_errposition(@6)));
> +					n->quotes = $6;
> +					n->on_empty = $7;
> +					n->location = @1;
> +					$$ = (Node *) n;
> +				}
> +			| JSON_QUERY '('
> +				json_api_common_syntax
> +				json_returning_clause_opt
> +				json_wrapper_behavior
> +				json_quotes_clause_opt
> +				json_query_behavior ON ERROR_P
> +			')'
> +				{
> +					JsonFuncExpr *n = makeNode(JsonFuncExpr);
> +
> +					n->op = JSON_QUERY_OP;
> +					n->common = (JsonCommon *) $3;
> +					n->output = (JsonOutput *) $4;
> +					n->wrapper = $5;
> +					if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC)
> +						ereport(ERROR,
> +								(errcode(ERRCODE_SYNTAX_ERROR),
> +								 errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"),
> +								 parser_errposition(@6)));
> +					n->quotes = $6;
> +					n->on_error = $7;
> +					n->location = @1;
> +					$$ = (Node *) n;
> +				}
> +			| JSON_QUERY '('
> +				json_api_common_syntax
> +				json_returning_clause_opt
> +				json_wrapper_behavior
> +				json_quotes_clause_opt
> +				json_query_behavior ON EMPTY_P
> +				json_query_behavior ON ERROR_P
> +			')'
> +				{
> +					JsonFuncExpr *n = makeNode(JsonFuncExpr);
> +
> +					n->op = JSON_QUERY_OP;
> +					n->common = (JsonCommon *) $3;
> +					n->output = (JsonOutput *) $4;
> +					n->wrapper = $5;
> +					if (n->wrapper != JSW_NONE && $6 != JS_QUOTES_UNSPEC)
> +						ereport(ERROR,
> +								(errcode(ERRCODE_SYNTAX_ERROR),
> +								 errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"),
> +								 parser_errposition(@6)));
> +					n->quotes = $6;
> +					n->on_empty = $7;
> +					n->on_error = $10;
> +					n->location = @1;
> +					$$ = (Node *) n;
> +				}

I'm sure we can find a way to deduplicate this.


> +			| JSON_EXISTS '('
> +				json_api_common_syntax
> +				json_returning_clause_opt
> +			')'
> +				{
> +					JsonFuncExpr *p = makeNode(JsonFuncExpr);
> +
> +					p->op = JSON_EXISTS_OP;
> +					p->common = (JsonCommon *) $3;
> +					p->output = (JsonOutput *) $4;
> +					p->location = @1;
> +					$$ = (Node *) p;
> +				}
> +			| JSON_EXISTS '('
> +				json_api_common_syntax
> +				json_returning_clause_opt
> +				json_exists_behavior ON ERROR_P
> +			')'
> +				{
> +					JsonFuncExpr *p = makeNode(JsonFuncExpr);
> +
> +					p->op = JSON_EXISTS_OP;
> +					p->common = (JsonCommon *) $3;
> +					p->output = (JsonOutput *) $4;
> +					p->on_error = $5;
> +					p->location = @1;
> +					$$ = (Node *) p;
> +				}
> +			| JSON_VALUE '('
> +				json_api_common_syntax
> +				json_returning_clause_opt
> +			')'
> +				{
> +					JsonFuncExpr *n = makeNode(JsonFuncExpr);
> +
> +					n->op = JSON_VALUE_OP;
> +					n->common = (JsonCommon *) $3;
> +					n->output = (JsonOutput *) $4;
> +					n->location = @1;
> +					$$ = (Node *) n;
> +				}
> +
> +			| JSON_VALUE '('
> +				json_api_common_syntax
> +				json_returning_clause_opt
> +				json_value_behavior ON EMPTY_P
> +			')'
> +				{
> +					JsonFuncExpr *n = makeNode(JsonFuncExpr);
> +
> +					n->op = JSON_VALUE_OP;
> +					n->common = (JsonCommon *) $3;
> +					n->output = (JsonOutput *) $4;
> +					n->on_empty = $5;
> +					n->location = @1;
> +					$$ = (Node *) n;
> +				}
> +			| JSON_VALUE '('
> +				json_api_common_syntax
> +				json_returning_clause_opt
> +				json_value_behavior ON ERROR_P
> +			')'
> +				{
> +					JsonFuncExpr *n = makeNode(JsonFuncExpr);
> +
> +					n->op = JSON_VALUE_OP;
> +					n->common = (JsonCommon *) $3;
> +					n->output = (JsonOutput *) $4;
> +					n->on_error = $5;
> +					n->location = @1;
> +					$$ = (Node *) n;
> +				}
> +
> +			| JSON_VALUE '('
> +				json_api_common_syntax
> +				json_returning_clause_opt
> +				json_value_behavior ON EMPTY_P
> +				json_value_behavior ON ERROR_P
> +			')'
> +				{
> +					JsonFuncExpr *n = makeNode(JsonFuncExpr);
> +
> +					n->op = JSON_VALUE_OP;
> +					n->common = (JsonCommon *) $3;
> +					n->output = (JsonOutput *) $4;
> +					n->on_empty = $5;
> +					n->on_error = $8;
> +					n->location = @1;
> +					$$ = (Node *) n;
> +				}
>  			;

And this.



> +json_query_behavior:
> +			ERROR_P		{ $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL, @1); }
> +			| NULL_P		{ $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL, @1); }
> +			| DEFAULT a_expr	{ $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2, @1); }
> +			| EMPTY_P ARRAY	{ $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL, @1); }
> +			| EMPTY_P OBJECT_P	{ $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL, @1); }
> +			/* non-standard, for Oracle compatibility only */
> +			| EMPTY_P		{ $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL, @1); }
> +		;


> +json_exists_behavior:
> +			ERROR_P		{ $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL, @1); }
> +			| TRUE_P		{ $$ = makeJsonBehavior(JSON_BEHAVIOR_TRUE, NULL, @1); }
> +			| FALSE_P		{ $$ = makeJsonBehavior(JSON_BEHAVIOR_FALSE, NULL, @1); }
> +			| UNKNOWN		{ $$ = makeJsonBehavior(JSON_BEHAVIOR_UNKNOWN, NULL, @1); }
> +		;
> +
> +json_value_behavior:
> +			NULL_P		{ $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL, @1); }
> +			| ERROR_P		{ $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL, @1); }
> +			| DEFAULT a_expr	{ $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2, @1); }
> +		;

This also seems like it could use some dedup.


Greetings,

Andres Freund





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-07 13:54  Alvaro Herrera <alvherre@alvh.no-ip.org>
  parent: Andres Freund <andres@anarazel.de>
  2 siblings, 0 replies; 300+ messages in thread

From: Alvaro Herrera @ 2023-10-07 13:54 UTC (permalink / raw)
  To: Andres Freund <andres@anarazel.de>; +Cc: Amit Langote <amitlangote09@gmail.com>; Andrew Dunstan <andrew@dunslane.net>; jian he <jian.universality@gmail.com>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

On 2023-Oct-06, Andres Freund wrote:

> > +json_query_behavior:
> > +			ERROR_P		{ $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL, @1); }
> > +			| NULL_P		{ $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL, @1); }
> > +			| DEFAULT a_expr	{ $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2, @1); }
> > +			| EMPTY_P ARRAY	{ $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL, @1); }
> > +			| EMPTY_P OBJECT_P	{ $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL, @1); }
> > +			/* non-standard, for Oracle compatibility only */
> > +			| EMPTY_P		{ $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL, @1); }
> > +		;
> 
> > +json_exists_behavior:
> > +			ERROR_P		{ $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL, @1); }
> > +			| TRUE_P		{ $$ = makeJsonBehavior(JSON_BEHAVIOR_TRUE, NULL, @1); }
> > +			| FALSE_P		{ $$ = makeJsonBehavior(JSON_BEHAVIOR_FALSE, NULL, @1); }
> > +			| UNKNOWN		{ $$ = makeJsonBehavior(JSON_BEHAVIOR_UNKNOWN, NULL, @1); }
> > +		;
> > +
> > +json_value_behavior:
> > +			NULL_P		{ $$ = makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL, @1); }
> > +			| ERROR_P		{ $$ = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL, @1); }
> > +			| DEFAULT a_expr	{ $$ = makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2, @1); }
> > +		;
> 
> This also seems like it could use some dedup.

Yeah, I was looking at this the other day and thinking that we should
just have a single json_behavior that's used by all these productions;
at runtime we can check whether a value has been used that's improper
for that particular node, and error out with a syntax error or some
such.

Other parts of the grammar definitely needs more work, too.  It appears
to me that they were written by looking at what the standard says, more
or less literally.

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"Someone said that it is at least an order of magnitude more work to do
production software than a prototype. I think he is wrong by at least
an order of magnitude."                              (Brian Kernighan)





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-11 05:08  Amit Langote <amitlangote09@gmail.com>
  parent: Andres Freund <andres@anarazel.de>
  2 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-10-11 05:08 UTC (permalink / raw)
  To: Andres Freund <andres@anarazel.de>; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; jian he <jian.universality@gmail.com>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

Hi Andres,

On Sat, Oct 7, 2023 at 6:49 AM Andres Freund <andres@anarazel.de> wrote:
> Hi,
>
> On 2023-09-29 13:57:46 +0900, Amit Langote wrote:
> > Thanks.  I will push the attached 0001 shortly.
>
> Sorry for not looking at this earlier.

Thanks for the review.  Replying here only to your comments on 0001.

> Have you done benchmarking to verify that 0001 does not cause performance
> regressions? I'd not be suprised if it did.

I found that it indeed did once I benchmarked with something that
would stress EEOP_IOCOERCE:

do $$
begin
for i in 1..20000000 loop
i := i::text;
end loop; end; $$ language plpgsql;
DO

Times and perf report:

HEAD:

Time: 1815.824 ms (00:01.816)
Time: 1818.980 ms (00:01.819)
Time: 1695.555 ms (00:01.696)
Time: 1762.022 ms (00:01.762)

                --97.49%--exec_stmts
                          |
                          --85.97%--exec_assign_expr
                                    |
                                    |--65.56%--exec_eval_expr
                                    |          |
                                    |          |--53.71%--ExecInterpExpr
                                    |          |          |
                                    |          |          |--14.14%--textin


Patched:

Time: 1872.469 ms (00:01.872)
Time: 1927.371 ms (00:01.927)
Time: 1910.126 ms (00:01.910)
Time: 1948.322 ms (00:01.948)

                --97.70%--exec_stmts
                          |
                          --88.13%--exec_assign_expr
                                    |
                                    |--73.27%--exec_eval_expr
                                    |          |
                                    |          |--58.29%--ExecInterpExpr
                                    |          |          |
                                    |          |
|--25.69%--InputFunctionCallSafe
                                    |          |          |          |
                                    |          |          |
|--14.75%--textin

So, yes, putting InputFunctionCallSafe() in the common path may not
have been such a good idea.

> I'd split the soft-error path into
> a separate opcode. For JIT it can largely be implemented using the same code,
> eliding the check if it's the non-soft path. Or you can just put it into an
> out-of-line function.

Do you mean putting the execExprInterp.c code for the soft-error path
(with a new opcode) into an out-of-line function?  That definitely
makes the JIT version a tad simpler than if the error-checking is done
in-line.

So, the existing code for EEOP_IOCOERCE in both execExprInterp.c and
llvmjit_expr.c will remain unchanged.  Also, I can write the code for
the new opcode such that it doesn't use InputFunctionCallSafe() at
runtime, but rather passes the ErrorSaveContext directly by putting
that in the input function's FunctionCallInfo.context and checking
SOFT_ERROR_OCCURRED() directly.  That will have less overhead.

> I don't like adding more stuff to ExprState. This one seems particularly
> awkward, because it might be used by more than one level in an expression
> subtree, which means you really need to save/restore old values when
> recursing.

Hmm, I'd think that all levels will follow either soft or non-soft
error mode, so sharing the ErrorSaveContext passed via ExprState
doesn't look wrong to me.  IOW, there's only one value, not one for
every level, so there doesn't appear to be any need to have the
save/restore convention as we have for innermost_domainval et al.

I can see your point that adding another 8 bytes at the end of
ExprState might be undesirable.  Note though that ExprState.escontext
is only accessed in the ExecInitExpr phase, but during evaluation.

The alternative to not passing the ErrorSaveContext via ExprState is
to add a new parameter to ExecInitExprRec() and to functions that call
it.  The footprint would be much larger though.  Would you rather
prefer that?

> > @@ -1579,25 +1582,13 @@ ExecInitExprRec(Expr *node, ExprState *state,
> >
> >                               /* lookup the result type's input function */
> >                               scratch.d.iocoerce.finfo_in = palloc0(sizeof(FmgrInfo));
> > -                             scratch.d.iocoerce.fcinfo_data_in = palloc0(SizeForFunctionCallInfo(3));
> > -
> >                               getTypeInputInfo(iocoerce->resulttype,
> > -                                                              &iofunc, &typioparam);
> > +                                                              &iofunc, &scratch.d.iocoerce.typioparam);
> >                               fmgr_info(iofunc, scratch.d.iocoerce.finfo_in);
> >                               fmgr_info_set_expr((Node *) node, scratch.d.iocoerce.finfo_in);
> > -                             InitFunctionCallInfoData(*scratch.d.iocoerce.fcinfo_data_in,
> > -                                                                              scratch.d.iocoerce.finfo_in,
> > -                                                                              3, InvalidOid, NULL, NULL);
> >
> > -                             /*
> > -                              * We can preload the second and third arguments for the input
> > -                              * function, since they're constants.
> > -                              */
> > -                             fcinfo_in = scratch.d.iocoerce.fcinfo_data_in;
> > -                             fcinfo_in->args[1].value = ObjectIdGetDatum(typioparam);
> > -                             fcinfo_in->args[1].isnull = false;
> > -                             fcinfo_in->args[2].value = Int32GetDatum(-1);
> > -                             fcinfo_in->args[2].isnull = false;
> > +                             /* Use the ErrorSaveContext passed by the caller. */
> > +                             scratch.d.iocoerce.escontext = state->escontext;
> >
> >                               ExprEvalPushStep(state, &scratch);
> >                               break;
>
> I think it's likely that removing the optimization of not needing to set these
> arguments ahead of time will result in a performance regression. Not to speak
> of initializing the fcinfo from scratch on every evaluation of the expression.

Yes, that's not good.  I agree with separating out the soft-error path.

I'll post the patch and benchmarking results with the new patch shortly.

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-11 12:34  Amit Langote <amitlangote09@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-10-11 12:34 UTC (permalink / raw)
  To: Andres Freund <andres@anarazel.de>; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; jian he <jian.universality@gmail.com>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

On Wed, Oct 11, 2023 at 2:08 PM Amit Langote <amitlangote09@gmail.com> wrote:
> On Sat, Oct 7, 2023 at 6:49 AM Andres Freund <andres@anarazel.de> wrote:
> > On 2023-09-29 13:57:46 +0900, Amit Langote wrote:
> > > Thanks.  I will push the attached 0001 shortly.
> >
> > Sorry for not looking at this earlier.
>
> Thanks for the review.  Replying here only to your comments on 0001.
>
> > Have you done benchmarking to verify that 0001 does not cause performance
> > regressions? I'd not be suprised if it did.
>
> I found that it indeed did once I benchmarked with something that
> would stress EEOP_IOCOERCE:
>
> do $$
> begin
> for i in 1..20000000 loop
> i := i::text;
> end loop; end; $$ language plpgsql;
> DO
>
> Times and perf report:
>
> HEAD:
>
> Time: 1815.824 ms (00:01.816)
> Time: 1818.980 ms (00:01.819)
> Time: 1695.555 ms (00:01.696)
> Time: 1762.022 ms (00:01.762)
>
>                 --97.49%--exec_stmts
>                           |
>                           --85.97%--exec_assign_expr
>                                     |
>                                     |--65.56%--exec_eval_expr
>                                     |          |
>                                     |          |--53.71%--ExecInterpExpr
>                                     |          |          |
>                                     |          |          |--14.14%--textin
>
>
> Patched:
>
> Time: 1872.469 ms (00:01.872)
> Time: 1927.371 ms (00:01.927)
> Time: 1910.126 ms (00:01.910)
> Time: 1948.322 ms (00:01.948)
>
>                 --97.70%--exec_stmts
>                           |
>                           --88.13%--exec_assign_expr
>                                     |
>                                     |--73.27%--exec_eval_expr
>                                     |          |
>                                     |          |--58.29%--ExecInterpExpr
>                                     |          |          |
>                                     |          |
> |--25.69%--InputFunctionCallSafe
>                                     |          |          |          |
>                                     |          |          |
> |--14.75%--textin
>
> So, yes, putting InputFunctionCallSafe() in the common path may not
> have been such a good idea.
>
> > I'd split the soft-error path into
> > a separate opcode. For JIT it can largely be implemented using the same code,
> > eliding the check if it's the non-soft path. Or you can just put it into an
> > out-of-line function.
>
> Do you mean putting the execExprInterp.c code for the soft-error path
> (with a new opcode) into an out-of-line function?  That definitely
> makes the JIT version a tad simpler than if the error-checking is done
> in-line.
>
> So, the existing code for EEOP_IOCOERCE in both execExprInterp.c and
> llvmjit_expr.c will remain unchanged.  Also, I can write the code for
> the new opcode such that it doesn't use InputFunctionCallSafe() at
> runtime, but rather passes the ErrorSaveContext directly by putting
> that in the input function's FunctionCallInfo.context and checking
> SOFT_ERROR_OCCURRED() directly.  That will have less overhead.
>
> > I don't like adding more stuff to ExprState. This one seems particularly
> > awkward, because it might be used by more than one level in an expression
> > subtree, which means you really need to save/restore old values when
> > recursing.
>
> Hmm, I'd think that all levels will follow either soft or non-soft
> error mode, so sharing the ErrorSaveContext passed via ExprState
> doesn't look wrong to me.  IOW, there's only one value, not one for
> every level, so there doesn't appear to be any need to have the
> save/restore convention as we have for innermost_domainval et al.
>
> I can see your point that adding another 8 bytes at the end of
> ExprState might be undesirable.  Note though that ExprState.escontext
> is only accessed in the ExecInitExpr phase, but during evaluation.
>
> The alternative to not passing the ErrorSaveContext via ExprState is
> to add a new parameter to ExecInitExprRec() and to functions that call
> it.  The footprint would be much larger though.  Would you rather
> prefer that?
>
> > > @@ -1579,25 +1582,13 @@ ExecInitExprRec(Expr *node, ExprState *state,
> > >
> > >                               /* lookup the result type's input function */
> > >                               scratch.d.iocoerce.finfo_in = palloc0(sizeof(FmgrInfo));
> > > -                             scratch.d.iocoerce.fcinfo_data_in = palloc0(SizeForFunctionCallInfo(3));
> > > -
> > >                               getTypeInputInfo(iocoerce->resulttype,
> > > -                                                              &iofunc, &typioparam);
> > > +                                                              &iofunc, &scratch.d.iocoerce.typioparam);
> > >                               fmgr_info(iofunc, scratch.d.iocoerce.finfo_in);
> > >                               fmgr_info_set_expr((Node *) node, scratch.d.iocoerce.finfo_in);
> > > -                             InitFunctionCallInfoData(*scratch.d.iocoerce.fcinfo_data_in,
> > > -                                                                              scratch.d.iocoerce.finfo_in,
> > > -                                                                              3, InvalidOid, NULL, NULL);
> > >
> > > -                             /*
> > > -                              * We can preload the second and third arguments for the input
> > > -                              * function, since they're constants.
> > > -                              */
> > > -                             fcinfo_in = scratch.d.iocoerce.fcinfo_data_in;
> > > -                             fcinfo_in->args[1].value = ObjectIdGetDatum(typioparam);
> > > -                             fcinfo_in->args[1].isnull = false;
> > > -                             fcinfo_in->args[2].value = Int32GetDatum(-1);
> > > -                             fcinfo_in->args[2].isnull = false;
> > > +                             /* Use the ErrorSaveContext passed by the caller. */
> > > +                             scratch.d.iocoerce.escontext = state->escontext;
> > >
> > >                               ExprEvalPushStep(state, &scratch);
> > >                               break;
> >
> > I think it's likely that removing the optimization of not needing to set these
> > arguments ahead of time will result in a performance regression. Not to speak
> > of initializing the fcinfo from scratch on every evaluation of the expression.
>
> Yes, that's not good.  I agree with separating out the soft-error path.
>
> I'll post the patch and benchmarking results with the new patch shortly.

So here's 0001, rewritten to address the above comments.

It adds a new eval opcode EEOP_IOCOERCE_SAFE, which basically copies
the implementation of EEOP_IOCOERCE but passes the ErrorSaveContext
passed by the caller to the input function via the latter's
FunctionCallInfo.context.  However, unlike EEOP_IOCOERCE, it's
implemented in a separate function to encapsulate away the logic of
returning NULL when an error occurs.  This makes JITing much simpler,
because it now involves simply calling the function.

Here are the benchmark results:

Same DO block:

do $$
begin
for i in 1..20000000 loop
i := i::text;
end loop; end; $$ language plpgsql;

HEAD:
Time: 1629.461 ms (00:01.629)
Time: 1635.439 ms (00:01.635)
Time: 1634.432 ms (00:01.634)

Patched:
Time: 1657.657 ms (00:01.658)
Time: 1686.779 ms (00:01.687)
Time: 1626.985 ms (00:01.627)

Using the SQL/JSON query functions patch rebased over the new 0001, I
also compared the difference in performance between EEOP_IOCOERCE and
EEOP_IOCOERCE_SAFE:

-- uses EEOP_IOCOERCE because ERROR ON ERROR
do $$
begin
for i in 1..20000000 loop
i := JSON_VALUE(jsonb '1', '$' RETURNING text ERROR ON ERROR );
end loop; end; $$ language plpgsql;

-- uses EEOP_IOCOERCE because ERROR ON ERROR
do $$
begin
for i in 1..20000000 loop
i := JSON_VALUE(jsonb '1', '$' RETURNING text ERROR ON ERROR );
end loop; end; $$ language plpgsql;

Time: 2960.434 ms (00:02.960)
Time: 2968.895 ms (00:02.969)
Time: 3006.691 ms (00:03.007)

-- uses EEOP_IOCOERCE_SAFE because NULL ON ERROR
do $$
begin
for i in 1..20000000 loop
i := JSON_VALUE(jsonb '1', '$' RETURNING text NULL ON ERROR);
end loop; end; $$ language plpgsql;

Time: 3046.933 ms (00:03.047)
Time: 3073.385 ms (00:03.073)
Time: 3121.619 ms (00:03.122)

There's only a slight degradation with the SAFE variant presumably due
to the extra whether-error-occurred check after calling the input
function.  I'd think the difference would have been more pronounced
had I continued to use InputFunctionCallSafe().

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com

Attachments:

v23-0001-Add-soft-error-handling-to-some-expression-nodes.patch (application/octet-stream, 13.0K) download From 9b43676efa45878779dbd0ae98b0bac8d41cbd8a Mon Sep 17 00:00:00 2001 From: Amit Langote <amitlan@postgresql.org> Date: Fri, 29 Sep 2023 13:22:15 +0900 Subject: [PATCH v23] Add soft error handling to some expression nodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adjusts the CoerceViaIO and CoerceToDomain expression evaluation code to handle errors softly. For CoerceViaIo, this adds a new ExprEvalStep opcode EEOP_IOCOERCE_SAFE, which is implemented in new function ExecEvalCoerceViaIOSafe(). The only difference from EEOP_IOCOERCE's inline implementation is that the input function receives an ErrorSaveContext via the function's FunctionCallInfo.context, which it can use to handle errors softly. For CoerceToDomain, this simply entails replacing the ereport() in ExecEvalConstraintCheck() by errsave(). In both cases, the ErrorSaveContext to be used is populated in the expression's struct in the ExprEvalStep. The ErrorSaveContext can be passed by setting ExprState.escontext to point to it before calling ExecInitExprRec() on the expression tree whose errors are to be suppressed. Note that no call site of ExecInitExprRec() has been changed in this commit, so there's no functional change. This is intended for implementing new SQL/JSON expression nodes in future commits that will use to it suppress errors that may occur during type coercions. Reviewed-by: Álvaro Herrera Reviewed-by: Andres Freund Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com --- src/backend/executor/execExpr.c | 8 ++- src/backend/executor/execExprInterp.c | 72 ++++++++++++++++++++++++++- src/backend/jit/llvm/llvmjit.c | 4 ++ src/backend/jit/llvm/llvmjit_expr.c | 6 +++ src/backend/jit/llvm/llvmjit_types.c | 4 ++ src/include/executor/execExpr.h | 4 ++ src/include/jit/llvmjit.h | 2 + src/include/jit/llvmjit_emit.h | 9 ++++ src/include/nodes/execnodes.h | 7 +++ 9 files changed, 114 insertions(+), 2 deletions(-) diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 2c62b0c9c8..34bd2102b5 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1563,7 +1563,10 @@ ExecInitExprRec(Expr *node, ExprState *state, * We don't check permissions here as a type's input/output * function are assumed to be executable by everyone. */ - scratch.opcode = EEOP_IOCOERCE; + if (state->escontext == NULL) + scratch.opcode = EEOP_IOCOERCE; + else + scratch.opcode = EEOP_IOCOERCE_SAFE; /* lookup the source type's output function */ scratch.d.iocoerce.finfo_out = palloc0(sizeof(FmgrInfo)); @@ -1599,6 +1602,8 @@ ExecInitExprRec(Expr *node, ExprState *state, fcinfo_in->args[2].value = Int32GetDatum(-1); fcinfo_in->args[2].isnull = false; + fcinfo_in->context = (Node *) state->escontext; + ExprEvalPushStep(state, &scratch); break; } @@ -3306,6 +3311,7 @@ ExecInitCoerceToDomain(ExprEvalStep *scratch, CoerceToDomain *ctest, /* we'll allocate workspace only if needed */ scratch->d.domaincheck.checkvalue = NULL; scratch->d.domaincheck.checknull = NULL; + scratch->d.domaincheck.escontext = state->escontext; /* * Evaluate argument - it's fine to directly store it into resv/resnull, diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 24c2b60c62..4e152fdfe3 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -63,6 +63,7 @@ #include "executor/nodeSubplan.h" #include "funcapi.h" #include "miscadmin.h" +#include "nodes/miscnodes.h" #include "nodes/nodeFuncs.h" #include "parser/parsetree.h" #include "pgstat.h" @@ -452,6 +453,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_CASE_TESTVAL, &&CASE_EEOP_MAKE_READONLY, &&CASE_EEOP_IOCOERCE, + &&CASE_EEOP_IOCOERCE_SAFE, &&CASE_EEOP_DISTINCT, &&CASE_EEOP_NOT_DISTINCT, &&CASE_EEOP_NULLIF, @@ -1205,6 +1207,12 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_IOCOERCE_SAFE) + { + ExecEvalCoerceViaIOSafe(state, op); + EEO_NEXT(); + } + EEO_CASE(EEOP_DISTINCT) { /* @@ -2510,6 +2518,68 @@ ExecEvalParamExtern(ExprState *state, ExprEvalStep *op, ExprContext *econtext) errmsg("no value found for parameter %d", paramId))); } +/* + * Evaluate a CoerceViaIO node in soft-error mode. + * + * The source value is in op's result variable. + */ +void +ExecEvalCoerceViaIOSafe(ExprState *state, ExprEvalStep *op) +{ + char *str; + + /* call output function (similar to OutputFunctionCall) */ + if (*op->resnull) + { + /* output functions are not called on nulls */ + str = NULL; + } + else + { + FunctionCallInfo fcinfo_out; + + fcinfo_out = op->d.iocoerce.fcinfo_data_out; + fcinfo_out->args[0].value = *op->resvalue; + fcinfo_out->args[0].isnull = false; + + fcinfo_out->isnull = false; + str = DatumGetCString(FunctionCallInvoke(fcinfo_out)); + + /* OutputFunctionCall assumes result isn't null */ + Assert(!fcinfo_out->isnull); + } + + /* call input function (similar to InputFunctionCall) */ + if (!op->d.iocoerce.finfo_in->fn_strict || str != NULL) + { + FunctionCallInfo fcinfo_in; + + fcinfo_in = op->d.iocoerce.fcinfo_data_in; + fcinfo_in->args[0].value = PointerGetDatum(str); + fcinfo_in->args[0].isnull = *op->resnull; + /* second and third arguments are already set up */ + + /* ErrorSaveContext must be present. */ + Assert(IsA(fcinfo_in->context, ErrorSaveContext)); + + fcinfo_in->isnull = false; + *op->resvalue = FunctionCallInvoke(fcinfo_in); + + if (SOFT_ERROR_OCCURRED(fcinfo_in->context)) + { + *op->resnull = true; + *op->resvalue = (Datum) 0; + return; + } + + /* Should get null result if and only if str is NULL */ + if (str == NULL) + Assert(*op->resnull); + else + Assert(!*op->resnull); + } +} + /* * Evaluate a SQLValueFunction expression. */ @@ -3745,7 +3815,7 @@ ExecEvalConstraintCheck(ExprState *state, ExprEvalStep *op) { if (!*op->d.domaincheck.checknull && !DatumGetBool(*op->d.domaincheck.checkvalue)) - ereport(ERROR, + errsave((Node *) op->d.domaincheck.escontext, (errcode(ERRCODE_CHECK_VIOLATION), errmsg("value for domain %s violates check constraint \"%s\"", format_type_be(op->d.domaincheck.resulttype), diff --git a/src/backend/jit/llvm/llvmjit.c b/src/backend/jit/llvm/llvmjit.c index 4dfaf79743..5a9be73957 100644 --- a/src/backend/jit/llvm/llvmjit.c +++ b/src/backend/jit/llvm/llvmjit.c @@ -70,12 +70,14 @@ LLVMTypeRef StructHeapTupleTableSlot; LLVMTypeRef StructMinimalTupleTableSlot; LLVMTypeRef StructMemoryContextData; LLVMTypeRef StructFunctionCallInfoData; +LLVMTypeRef StructFmgrInfo; LLVMTypeRef StructExprContext; LLVMTypeRef StructExprEvalStep; LLVMTypeRef StructExprState; LLVMTypeRef StructAggState; LLVMTypeRef StructAggStatePerGroupData; LLVMTypeRef StructAggStatePerTransData; +LLVMTypeRef StructNode; LLVMValueRef AttributeTemplate; @@ -1118,6 +1120,7 @@ llvm_create_types(void) StructExprEvalStep = llvm_pg_var_type("StructExprEvalStep"); StructExprState = llvm_pg_var_type("StructExprState"); StructFunctionCallInfoData = llvm_pg_var_type("StructFunctionCallInfoData"); + StructFmgrInfo = llvm_pg_var_type("StructFmgrInfo"); StructMemoryContextData = llvm_pg_var_type("StructMemoryContextData"); StructTupleTableSlot = llvm_pg_var_type("StructTupleTableSlot"); StructHeapTupleTableSlot = llvm_pg_var_type("StructHeapTupleTableSlot"); @@ -1127,6 +1130,7 @@ llvm_create_types(void) StructAggState = llvm_pg_var_type("StructAggState"); StructAggStatePerGroupData = llvm_pg_var_type("StructAggStatePerGroupData"); StructAggStatePerTransData = llvm_pg_var_type("StructAggStatePerTransData"); + StructNode = llvm_pg_var_type("StructNode"); AttributeTemplate = LLVMGetNamedFunction(llvm_types_module, "AttributeTemplate"); } diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 4b51aa1ce0..750acea898 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -1379,6 +1379,12 @@ llvm_compile_expr(ExprState *state) break; } + case EEOP_IOCOERCE_SAFE: + build_EvalXFunc(b, mod, "ExecEvalCoerceViaIOSafe", + v_state, op); + LLVMBuildBr(b, opblocks[opno + 1]); + break; + case EEOP_DISTINCT: case EEOP_NOT_DISTINCT: { diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c index 41ac4c6f45..c034533dc0 100644 --- a/src/backend/jit/llvm/llvmjit_types.c +++ b/src/backend/jit/llvm/llvmjit_types.c @@ -59,6 +59,7 @@ AggStatePerTransData StructAggStatePerTransData; ExprContext StructExprContext; ExprEvalStep StructExprEvalStep; ExprState StructExprState; +FmgrInfo StructFmgrInfo; FunctionCallInfoBaseData StructFunctionCallInfoData; HeapTupleData StructHeapTupleData; MemoryContextData StructMemoryContextData; @@ -66,6 +67,7 @@ TupleTableSlot StructTupleTableSlot; HeapTupleTableSlot StructHeapTupleTableSlot; MinimalTupleTableSlot StructMinimalTupleTableSlot; TupleDescData StructTupleDescData; +Node StructNode; /* @@ -126,6 +128,7 @@ void *referenced_functions[] = ExecEvalRow, ExecEvalRowNotNull, ExecEvalRowNull, + ExecEvalCoerceViaIOSafe, ExecEvalSQLValueFunction, ExecEvalScalarArrayOp, ExecEvalHashedScalarArrayOp, @@ -136,6 +139,7 @@ void *referenced_functions[] = ExecEvalJsonConstructor, ExecEvalJsonIsPredicate, MakeExpandedObjectReadOnlyInternal, + InputFunctionCallSafe, slot_getmissingattrs, slot_getsomeattrs_int, strlen, diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index 048573c2bc..c4fd933154 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -16,6 +16,7 @@ #include "executor/nodeAgg.h" #include "nodes/execnodes.h" +#include "nodes/miscnodes.h" /* forward references to avoid circularity */ struct ExprEvalStep; @@ -168,6 +169,7 @@ typedef enum ExprEvalOp /* evaluate assorted special-purpose expression types */ EEOP_IOCOERCE, + EEOP_IOCOERCE_SAFE, EEOP_DISTINCT, EEOP_NOT_DISTINCT, EEOP_NULLIF, @@ -547,6 +549,7 @@ typedef struct ExprEvalStep bool *checknull; /* OID of domain type */ Oid resulttype; + ErrorSaveContext *escontext; } domaincheck; /* for EEOP_CONVERT_ROWTYPE */ @@ -776,6 +779,7 @@ extern void ExecEvalParamExec(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalParamExtern(ExprState *state, ExprEvalStep *op, ExprContext *econtext); +extern void ExecEvalCoerceViaIOSafe(ExprState *state, ExprEvalStep *op); extern void ExecEvalSQLValueFunction(ExprState *state, ExprEvalStep *op); extern void ExecEvalCurrentOfExpr(ExprState *state, ExprEvalStep *op); extern void ExecEvalNextValueExpr(ExprState *state, ExprEvalStep *op); diff --git a/src/include/jit/llvmjit.h b/src/include/jit/llvmjit.h index 6d90a16f79..e4ff3be566 100644 --- a/src/include/jit/llvmjit.h +++ b/src/include/jit/llvmjit.h @@ -75,6 +75,7 @@ extern PGDLLIMPORT LLVMTypeRef StructTupleTableSlot; extern PGDLLIMPORT LLVMTypeRef StructHeapTupleTableSlot; extern PGDLLIMPORT LLVMTypeRef StructMinimalTupleTableSlot; extern PGDLLIMPORT LLVMTypeRef StructMemoryContextData; +extern PGDLLIMPORT LLVMTypeRef StructFmgrInfo; extern PGDLLIMPORT LLVMTypeRef StructFunctionCallInfoData; extern PGDLLIMPORT LLVMTypeRef StructExprContext; extern PGDLLIMPORT LLVMTypeRef StructExprEvalStep; @@ -82,6 +83,7 @@ extern PGDLLIMPORT LLVMTypeRef StructExprState; extern PGDLLIMPORT LLVMTypeRef StructAggState; extern PGDLLIMPORT LLVMTypeRef StructAggStatePerTransData; extern PGDLLIMPORT LLVMTypeRef StructAggStatePerGroupData; +extern PGDLLIMPORT LLVMTypeRef StructNode; extern PGDLLIMPORT LLVMValueRef AttributeTemplate; diff --git a/src/include/jit/llvmjit_emit.h b/src/include/jit/llvmjit_emit.h index 5e74543be4..ead46a64ae 100644 --- a/src/include/jit/llvmjit_emit.h +++ b/src/include/jit/llvmjit_emit.h @@ -85,6 +85,15 @@ l_sizet_const(size_t i) return LLVMConstInt(TypeSizeT, i, false); } +/* + * Emit constant oid. + */ +static inline LLVMValueRef +l_oid_const(LLVMContextRef lc, Oid i) +{ + return LLVMConstInt(LLVMInt32TypeInContext(lc), i, false); +} + /* * Emit constant boolean, as used for storage (e.g. global vars, structs). */ diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 108d69ba28..24e55c4578 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -34,6 +34,7 @@ #include "fmgr.h" #include "lib/ilist.h" #include "lib/pairingheap.h" +#include "nodes/miscnodes.h" #include "nodes/params.h" #include "nodes/plannodes.h" #include "nodes/tidbitmap.h" @@ -129,6 +130,12 @@ typedef struct ExprState Datum *innermost_domainval; bool *innermost_domainnull; + + /* + * For expression nodes that support soft errors. Should be set to NULL + * before calling ExecInitExprRec() if the caller wants errors thrown. + */ + ErrorSaveContext *escontext; } ExprState; -- 2.35.3
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-16 08:20  Nikita Malakhov <hukutoc@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 2 replies; 300+ messages in thread

From: Nikita Malakhov @ 2023-10-16 08:20 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Andres Freund <andres@anarazel.de>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; jian he <jian.universality@gmail.com>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

Hi!

With the latest set of patches we encountered failure with the following
query:

postgres@postgres=# SELECT JSON_QUERY(jsonpath '"aaa"', '$' RETURNING text);
server closed the connection unexpectedly
        This probably means the server terminated abnormally
        before or while processing the request.
The connection to the server was lost. Attempting reset: Failed.
The connection to the server was lost. Attempting reset: Failed.
Time: 11.165 ms

A colleague of mine, Anton Melnikov, proposed the following changes which
slightly
alter coercion functions to process this kind of error correctly.

Please check attached patch set.

--
Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

Attachments: download all (2 files, .tar.gz)

v23-0003-1-transformJsonExprCommon-fixup.patch (application/octet-stream, 958B) download From 0837459ad506d719dde10983a07d1d3413870f89 Mon Sep 17 00:00:00 2001 From: Nikita Malakhov <n.malakhov@postgrespro.ru> Date: Mon, 16 Oct 2023 00:15:40 +0300 Subject: [PATCH] SQL/JSON fix for SQL/JSON query functions failures with LLVM: Use JS_FORMAT_JSON as default in transformJsonExprCommon() --- src/backend/parser/parse_expr.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 21979fd64f..e62794dee5 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4370,7 +4370,7 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func, jsexpr->op = func->op; jsexpr->formatted_expr = transformJsonValueExpr(pstate, constructName, func->common->expr, - JS_FORMAT_DEFAULT, + JS_FORMAT_JSON, InvalidOid, false); jsexpr->format = func->common->expr->format; -- 2.25.1
v23-0003-2-json-query-coercion-override.patch (application/octet-stream, 17.1K) download From 529f85684774cb70e85af5671a7edf3526b96393 Mon Sep 17 00:00:00 2001 From: Nikita Malakhov <n.malakhov@postgrespro.ru> Date: Mon, 16 Oct 2023 00:24:01 +0300 Subject: [PATCH] Add check whether it is needed to override default coercion in JSON_QUERY function with OMIT QUOTES case, and checks is scalar types have error safe input/coercion functions, with test cases correction according to slightly changed behavior --- src/backend/executor/execExprInterp.c | 33 +++++++ src/backend/parser/parse_expr.c | 96 ++++++++++++++++++--- src/test/regress/expected/jsonb_sqljson.out | 43 ++++++--- src/test/regress/sql/jsonb_sqljson.sql | 16 +++- 4 files changed, 162 insertions(+), 26 deletions(-) diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 9adf31682c..cd92f920e4 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -4176,6 +4176,28 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op) *op->resvalue = BoolGetDatum(res); } +/* + * Check whether we need to override default coercion in + * JSON_QUERY(OMIT QUOTES) case. + */ +static bool +ExecJsonQueryNeedsIOCoercion(JsonExpr *jsexpr, Datum *res, bool isnull) +{ + if (jsexpr->omit_quotes && !isnull) + { + Jsonb *jb = DatumGetJsonbP(*res); + JsonbValue jbv; + + /* Coerce non-null scalar items via I/O in OMIT QUOTES case */ + if (JB_ROOT_IS_SCALAR(jb) && + JsonbExtractScalar(&jb->root, &jbv) && + jbv.type == jbvString) + return true; + } + + return false; +} + /* * Evaluate given JsonExpr by performing the specified JSON operation. * @@ -4236,7 +4258,18 @@ ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) *op->resvalue = (Datum) 0; return; } + resnull = !DatumGetPointer(res); + + /* Check coercion in OMIT QUOTES case */ + if (post_eval->jcstate && post_eval->jcstate->coercion && + post_eval->jcstate->coercion->via_io && + jexpr->on_error && jexpr->on_error->btype != JSON_BEHAVIOR_NULL) + if(!ExecJsonQueryNeedsIOCoercion(jexpr, &res, resnull)) + ereport(ERROR, + (errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE), + errmsg("SQL/JSON item cannot be cast to target type"))); + break; case JSON_VALUE_OP: diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index e62794dee5..58549461a4 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4304,7 +4304,6 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) ret->typid = JsonFuncExprDefaultReturnType(jsexpr); ret->typmod = -1; } - jsexpr->result_coercion = coerceJsonFuncExprOutput(pstate, jsexpr); jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, JSON_BEHAVIOR_NULL, @@ -4312,6 +4311,9 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, JSON_BEHAVIOR_NULL, jsexpr->returning); + + jsexpr->result_coercion = coerceJsonFuncExprOutput(pstate, jsexpr); + break; case JSON_VALUE_OP: @@ -4325,6 +4327,14 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->returning->typid = TEXTOID; jsexpr->returning->typmod = -1; } + + jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, + JSON_BEHAVIOR_NULL, + jsexpr->returning); + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_NULL, + jsexpr->returning); + jsexpr->result_coercion = coerceJsonFuncExprOutput(pstate, jsexpr); /* @@ -4335,12 +4345,6 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) InitJsonItemCoercions(pstate, jsexpr->returning, exprType(jsexpr->formatted_expr)); - jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, - JSON_BEHAVIOR_NULL, - jsexpr->returning); - jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, - JSON_BEHAVIOR_NULL, - jsexpr->returning); break; } @@ -4416,6 +4420,38 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func, return jsexpr; } +/* + * Check whether scalar type is required by SQL/JSON standard and has + * error-safe input/conversion functions. + */ +static bool +scalarTypeIsErrorSafe(Oid typid) +{ + switch (typid) + { + case JSONBOID: + case JSONOID: + case TEXTOID: + case VARCHAROID: + case BPCHAROID: + case BOOLOID: + case NUMERICOID: + case INT2OID: + case INT4OID: + case INT8OID: + case FLOAT4OID: + case FLOAT8OID: + case DATEOID: + case TIMEOID: + case TIMETZOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + return true; + default: + return false; + } +} + /* * Transform a JSON PASSING clause. */ @@ -4452,6 +4488,11 @@ coerceJsonFuncExprOutput(ParseState *pstate, JsonExpr *jsexpr) Node *context_item = jsexpr->formatted_expr; int default_typmod; Oid default_typid; + CaseTestExpr *placeholder = makeNode(CaseTestExpr); + + placeholder->typeId = exprType(context_item); + placeholder->typeMod = exprTypmod(context_item); + placeholder->collation = exprCollation(context_item); Assert(returning); @@ -4462,6 +4503,29 @@ coerceJsonFuncExprOutput(ParseState *pstate, JsonExpr *jsexpr) if (returning->typid != JSONOID && returning->typid != JSONBOID && (jsexpr->op != JSON_QUERY_OP || jsexpr->omit_quotes)) { + if (!scalarTypeIsErrorSafe(getBaseType(returning->typid)) && + (!jsexpr->on_error || + jsexpr->on_error->btype != JSON_BEHAVIOR_ERROR)) + { + if (jsexpr->op == JSON_QUERY_OP) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("returning type %s is not supported in JSON_QUERY() without ERROR ON ERROR", + format_type_be(jsexpr->returning->typid)), + parser_coercion_errposition(pstate, + exprLocation((Node *)placeholder), + (Node *) jsexpr))); + + if (jsexpr->op == JSON_VALUE_OP) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("returning type %s is not supported in JSON_VALUE() without ERROR ON ERROR", + format_type_be(jsexpr->returning->typid)), + parser_coercion_errposition(pstate, + exprLocation((Node *)placeholder), + (Node *) jsexpr))); + } + coercion = makeNode(JsonCoercion); coercion->expr = NULL; coercion->via_io = true; @@ -4487,16 +4551,24 @@ coerceJsonFuncExprOutput(ParseState *pstate, JsonExpr *jsexpr) * evaluating the JSON_VALUE/QUERY jsonpath expression to the coercion * function. */ - CaseTestExpr *placeholder = makeNode(CaseTestExpr); - - placeholder->typeId = exprType(context_item); - placeholder->typeMod = exprTypmod(context_item); - placeholder->collation = exprCollation(context_item); Assert(placeholder->typeId == default_typid); Assert(placeholder->typeMod == default_typmod); coercion = coerceJsonExpr(pstate, (Node *) placeholder, returning); + + if (coercion->via_io && !jsexpr->omit_quotes) + { + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot cast type %s to %s", + format_type_be(exprType(context_item)), + format_type_be(jsexpr->returning->typid)), + errhint("Try to use OMIT QUOTES clause in JSON_QUERY()."), + parser_coercion_errposition(pstate, + exprLocation((Node *)placeholder), + (Node *) jsexpr))); + } } return coercion; diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out index 8dcdb90fc8..e9ff4f67b1 100644 --- a/src/test/regress/expected/jsonb_sqljson.out +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -358,20 +358,28 @@ SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; -- Test NULL checks execution in domain types CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; -SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING sqljsonb_int_not_null); ERROR: domain sqljsonb_int_not_null does not allow null values -SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING sqljsonb_int_not_null ERROR ON ERROR); ERROR: domain sqljsonb_int_not_null does not allow null values -SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING sqljsonb_int_not_null DEFAULT 2 ON EMPTY ERROR ON ERROR); ERROR: domain sqljsonb_int_not_null does not allow null values -CREATE TYPE rainbow AS ENUM ('red', 'orange', 'yellow', 'green', 'blue', 'purple'); -CREATE DOMAIN rgb AS rainbow CHECK (VALUE IN ('red', 'green', 'blue')); -SELECT JSON_VALUE('"purple"'::jsonb, 'lax $[*]' RETURNING rgb); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT 2 ON EMPTY ERROR ON ERROR); json_value ------------ - + 2 (1 row) +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON EMPTY ERROR ON ERROR); +ERROR: domain sqljsonb_int_not_null does not allow null values +CREATE TYPE rainbow AS ENUM ('red', 'orange', 'yellow', 'green', 'blue', 'purple'); +CREATE DOMAIN rgb AS rainbow CHECK (VALUE IN ('red', 'green', 'blue')); +SELECT JSON_VALUE('"purple"'::jsonb, 'lax $[*]' RETURNING rgb); +ERROR: returning type rgb is not supported in JSON_VALUE() without ERROR ON ERROR +LINE 1: SELECT JSON_VALUE('"purple"'::jsonb, 'lax $[*]' RETURNING rg... + ^ +SELECT JSON_VALUE('"purple"'::jsonb, 'lax $[*]' RETURNING rgb ERROR ON ERROR); +ERROR: value for domain rgb violates check constraint "rgb_check" SELECT JSON_VALUE(jsonb '[]', '$'); json_value ------------ @@ -500,6 +508,10 @@ SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); (1 row) SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); +ERROR: returning type point is not supported in JSON_VALUE() without ERROR ON ERROR +LINE 1: SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )... + ^ +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point ERROR ON ERROR); json_value ------------ (1,2) @@ -907,6 +919,17 @@ SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {" 2 | | | [{}, true] | (2 rows) +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING jsonpath); +ERROR: cannot cast type jsonb to jsonpath +LINE 1: SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "... + ^ +HINT: Try to use OMIT QUOTES clause in JSON_QUERY(). +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING jsonpath OMIT QUOTES); +ERROR: returning type jsonpath is not supported in JSON_QUERY() without ERROR ON ERROR +LINE 1: SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "... + ^ +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING jsonpath OMIT QUOTES ERROR ON ERROR); +ERROR: SQL/JSON item cannot be cast to target type -- Extension: array types returning SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); json_query @@ -967,7 +990,7 @@ CREATE TABLE test_jsonb_constraints ( CONSTRAINT test_jsonb_constraint2 CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) CONSTRAINT test_jsonb_constraint3 - CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT '12' ON EMPTY ERROR ON ERROR) > i) CONSTRAINT test_jsonb_constraint4 CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') CONSTRAINT test_jsonb_constraint5 @@ -985,7 +1008,7 @@ CREATE TABLE test_jsonb_constraints ( Check constraints: "test_jsonb_constraint1" CHECK (js IS JSON) "test_jsonb_constraint2" CHECK (JSON_EXISTS(js::jsonb, '$."a"' PASSING i + 5 AS int, i::text AS txt, ARRAY[1, 2, 3] AS arr)) - "test_jsonb_constraint3" CHECK (JSON_VALUE(js::jsonb, '$."a"' RETURNING integer DEFAULT ('12'::text || i)::integer ON EMPTY ERROR ON ERROR) > i) + "test_jsonb_constraint3" CHECK (JSON_VALUE(js::jsonb, '$."a"' RETURNING integer DEFAULT 12 ON EMPTY ERROR ON ERROR) > i) "test_jsonb_constraint4" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb) "test_jsonb_constraint5" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C")) "test_jsonb_constraint6" CHECK (JSON_EXISTS(js::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2) @@ -999,7 +1022,7 @@ ORDER BY 1; (JSON_EXISTS((js)::jsonb, 'strict $."a"' RETURNING integer TRUE ON ERROR) < 2) (JSON_QUERY((js)::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C")) (JSON_QUERY((js)::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb) - (JSON_VALUE((js)::jsonb, '$."a"' RETURNING integer DEFAULT (('12'::text || i))::integer ON EMPTY ERROR ON ERROR) > i) + (JSON_VALUE((js)::jsonb, '$."a"' RETURNING integer DEFAULT 12 ON EMPTY ERROR ON ERROR) > i) (js IS JSON) JSON_EXISTS((js)::jsonb, '$."a"' PASSING (i + 5) AS int, (i)::text AS txt, ARRAY[1, 2, 3] AS arr) (6 rows) diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql index 6c3f8c7e43..a410369535 100644 --- a/src/test/regress/sql/jsonb_sqljson.sql +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -87,12 +87,15 @@ SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9; -- Test NULL checks execution in domain types CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL; -SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null); -SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null NULL ON ERROR); -SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING sqljsonb_int_not_null); +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING sqljsonb_int_not_null ERROR ON ERROR); +SELECT JSON_VALUE(jsonb 'null', '$' RETURNING sqljsonb_int_not_null DEFAULT 2 ON EMPTY ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT 2 ON EMPTY ERROR ON ERROR); +SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON EMPTY ERROR ON ERROR); CREATE TYPE rainbow AS ENUM ('red', 'orange', 'yellow', 'green', 'blue', 'purple'); CREATE DOMAIN rgb AS rainbow CHECK (VALUE IN ('red', 'green', 'blue')); SELECT JSON_VALUE('"purple"'::jsonb, 'lax $[*]' RETURNING rgb); +SELECT JSON_VALUE('"purple"'::jsonb, 'lax $[*]' RETURNING rgb ERROR ON ERROR); SELECT JSON_VALUE(jsonb '[]', '$'); SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR); @@ -135,6 +138,7 @@ FROM SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a); SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point); +SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point ERROR ON ERROR); -- Test timestamptz passing and output SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); @@ -259,6 +263,10 @@ SELECT JSON_QUERY(jsonb '[{"a": "a", "b": "foo", "t": "aaa", "js": [1, "2", {}], SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa); SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca); +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING jsonpath); +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING jsonpath OMIT QUOTES); +SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING jsonpath OMIT QUOTES ERROR ON ERROR); + -- Extension: array types returning SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER); SELECT JSON_QUERY(jsonb '[1,2,null,"a"]', '$[*]' RETURNING int[] WITH WRAPPER ERROR ON ERROR); @@ -285,7 +293,7 @@ CREATE TABLE test_jsonb_constraints ( CONSTRAINT test_jsonb_constraint2 CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr)) CONSTRAINT test_jsonb_constraint3 - CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT ('12' || i)::int ON EMPTY ERROR ON ERROR) > i) + CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT '12' ON EMPTY ERROR ON ERROR) > i) CONSTRAINT test_jsonb_constraint4 CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]') CONSTRAINT test_jsonb_constraint5 -- 2.25.1
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-16 08:33  Nikita Malakhov <hukutoc@gmail.com>
  parent: Nikita Malakhov <hukutoc@gmail.com>
  1 sibling, 1 reply; 300+ messages in thread

From: Nikita Malakhov @ 2023-10-16 08:33 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Andres Freund <andres@anarazel.de>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; jian he <jian.universality@gmail.com>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

Hi,

Also FYI - the following case results in segmentation fault:

postgres@postgres=# CREATE TABLE test_jsonb_constraints (
        js text,
        i int,
        x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER)
        CONSTRAINT test_jsonb_constraint1
                CHECK (js IS JSON)
        CONSTRAINT test_jsonb_constraint5
                CHECK (JSON_QUERY(js::jsonb, '$.mm' RETURNING char(5) OMIT
QUOTES EMPTY ARRAY ON EMPTY) >  'a' COLLATE "C")
        CONSTRAINT test_jsonb_constraint6
                CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int
TRUE ON ERROR) < 2)
);
CREATE TABLE
Time: 13.518 ms
postgres@postgres=# INSERT INTO test_jsonb_constraints VALUES ('[]');
server closed the connection unexpectedly
        This probably means the server terminated abnormally
        before or while processing the request.
The connection to the server was lost. Attempting reset: Failed.
The connection to the server was lost. Attempting reset: Failed.
Time: 6.858 ms
@!>

We're currently looking into this case.

--
Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-16 09:47  Amit Langote <amitlangote09@gmail.com>
  parent: Nikita Malakhov <hukutoc@gmail.com>
  0 siblings, 2 replies; 300+ messages in thread

From: Amit Langote @ 2023-10-16 09:47 UTC (permalink / raw)
  To: Nikita Malakhov <hukutoc@gmail.com>; +Cc: Andres Freund <andres@anarazel.de>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; jian he <jian.universality@gmail.com>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

Hi,

On Mon, Oct 16, 2023 at 5:34 PM Nikita Malakhov <hukutoc@gmail.com> wrote:
>
> Hi,
>
> Also FYI - the following case results in segmentation fault:
>
> postgres@postgres=# CREATE TABLE test_jsonb_constraints (
>         js text,
>         i int,
>         x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER)
>         CONSTRAINT test_jsonb_constraint1
>                 CHECK (js IS JSON)
>         CONSTRAINT test_jsonb_constraint5
>                 CHECK (JSON_QUERY(js::jsonb, '$.mm' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) >  'a' COLLATE "C")
>         CONSTRAINT test_jsonb_constraint6
>                 CHECK (JSON_EXISTS(js::jsonb, 'strict $.a' RETURNING int TRUE ON ERROR) < 2)
> );
> CREATE TABLE
> Time: 13.518 ms
> postgres@postgres=# INSERT INTO test_jsonb_constraints VALUES ('[]');
> server closed the connection unexpectedly
>         This probably means the server terminated abnormally
>         before or while processing the request.
> The connection to the server was lost. Attempting reset: Failed.
> The connection to the server was lost. Attempting reset: Failed.
> Time: 6.858 ms
> @!>
>
> We're currently looking into this case.

Thanks for the report.  I think I've figured out the problem --
ExecEvalJsonExprCoercion() mishandles the EMPTY ARRAY ON EMPTY case.

I'm reading the other 2 patches...

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-16 09:59  Nikita Malakhov <hukutoc@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  1 sibling, 0 replies; 300+ messages in thread

From: Nikita Malakhov @ 2023-10-16 09:59 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Andres Freund <andres@anarazel.de>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; jian he <jian.universality@gmail.com>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

Hi,

Sorry, forgot to mention above - patches from our patch set should be
applied
onto SQL/JSON part 3 - v22-0003-SQL-JSON-query-functions.patch, thus
they are numbered as v23-0003-1 and -2.

--
Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-16 12:49  jian he <jian.universality@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  1 sibling, 1 reply; 300+ messages in thread

From: jian he @ 2023-10-16 12:49 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Nikita Malakhov <hukutoc@gmail.com>; Andres Freund <andres@anarazel.de>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

On Mon, Oct 16, 2023 at 5:47 PM Amit Langote <amitlangote09@gmail.com> wrote:
>
> > We're currently looking into this case.
>
> Thanks for the report.  I think I've figured out the problem --
> ExecEvalJsonExprCoercion() mishandles the EMPTY ARRAY ON EMPTY case.
>
> I'm reading the other 2 patches...
>
> --
> Thanks, Amit Langote
> EDB: http://www.enterprisedb.com

query: select JSON_QUERY('[]'::jsonb, '$.mm' RETURNING text OMIT
QUOTES EMPTY ON EMPTY);

Breakpoint 2, ExecEvalJsonExpr (state=0x55e47ad685c0,
op=0x55e47ad68818, econtext=0x55e47ad682e8) at
../../Desktop/pg_sources/main/postgres/src/backend/executor/execExprInterp.c:4188
4188            JsonExprState *jsestate = op->d.jsonexpr.jsestate;
(gdb) fin
Run till exit from #0  ExecEvalJsonExpr (state=0x55e47ad685c0,
    op=0x55e47ad68818, econtext=0x55e47ad682e8)
    at ../../Desktop/pg_sources/main/postgres/src/backend/executor/execExprInterp.c:4188
ExecInterpExpr (state=0x55e47ad685c0, econtext=0x55e47ad682e8,
isnull=0x7ffe63659e2f) at
../../Desktop/pg_sources/main/postgres/src/backend/executor/execExprInterp.c:1556
1556                            EEO_NEXT();
(gdb) p *op->resnull
$1 = true
(gdb) cont
Continuing.

Breakpoint 1, ExecEvalJsonExprCoercion (state=0x55e47ad685c0,
op=0x55e47ad68998, econtext=0x55e47ad682e8, res=94439801785192,
resnull=false) at
../../Desktop/pg_sources/main/postgres/src/backend/executor/execExprInterp.c:4453
4453    {
(gdb) i args
state = 0x55e47ad685c0
op = 0x55e47ad68998
econtext = 0x55e47ad682e8
res = 94439801785192
resnull = false
(gdb) p *op->resnull
$2 = false
-------------------------------------------------------
in ExecEvalJsonExpr, *op->resnull is true.
then in ExecEvalJsonExprCoercion *op->resnull is false.
I am not sure why *op->resnull value changes, when changes.
-------------------------------------------------------
in ExecEvalJsonExprCoercion, if resnull is true, then jb is null, but
it seems there is no code to handle the case.
-----------------------------
add the following code after ExecEvalJsonExprCoercion if
(!InputFunctionCallSafe(...) works, but seems like a hack.

if (!val_string)
{
*op->resnull = true;
*op->resvalue = (Datum) 0;
}





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-16 13:44  Anton A. Melnikov <a.melnikov@postgrespro.ru>
  parent: jian he <jian.universality@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Anton A. Melnikov @ 2023-10-16 13:44 UTC (permalink / raw)
  To: jian he <jian.universality@gmail.com>; Amit Langote <amitlangote09@gmail.com>; +Cc: Nikita Malakhov <hukutoc@gmail.com>; Andres Freund <andres@anarazel.de>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

Hello!

On 16.10.2023 15:49, jian he wrote:
> add the following code after ExecEvalJsonExprCoercion if
> (!InputFunctionCallSafe(...) works, but seems like a hack.
> 
> if (!val_string)
> {
> *op->resnull = true;
> *op->resvalue = (Datum) 0;
> }

It seems the constraint should work here:

After

CREATE TABLE test (
	js text,
	i int,
	x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER)
	CONSTRAINT test_constraint
		CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a')
);

INSERT INTO test_jsonb_constraints VALUES ('[]');

one expected to see an error like that:

ERROR:  new row for relation "test" violates check constraint "test_constraint"
DETAIL:  Failing row contains ([], null, [1, 2]).

not "INSERT 0 1"

With best regards,

-- 
Anton A. Melnikov
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-17 04:02  Amit Langote <amitlangote09@gmail.com>
  parent: Anton A. Melnikov <a.melnikov@postgrespro.ru>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-10-17 04:02 UTC (permalink / raw)
  To: Anton A. Melnikov <a.melnikov@postgrespro.ru>; +Cc: jian he <jian.universality@gmail.com>; Nikita Malakhov <hukutoc@gmail.com>; Andres Freund <andres@anarazel.de>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

On Mon, Oct 16, 2023 at 10:44 PM Anton A. Melnikov
<a.melnikov@postgrespro.ru> wrote:
> On 16.10.2023 15:49, jian he wrote:
> > add the following code after ExecEvalJsonExprCoercion if
> > (!InputFunctionCallSafe(...) works, but seems like a hack.
> >
> > if (!val_string)
> > {
> > *op->resnull = true;
> > *op->resvalue = (Datum) 0;
> > }
>
> It seems the constraint should work here:
>
> After
>
> CREATE TABLE test (
>         js text,
>         i int,
>         x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER)
>         CONSTRAINT test_constraint
>                 CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a')
> );
>
> INSERT INTO test_jsonb_constraints VALUES ('[]');
>
> one expected to see an error like that:
>
> ERROR:  new row for relation "test" violates check constraint "test_constraint"
> DETAIL:  Failing row contains ([], null, [1, 2]).
>
> not "INSERT 0 1"

Yes, the correct thing here is for the constraint to fail.

One thing jian he missed during the debugging is that
ExecEvalJsonExprCoersion() receives the EMPTY ARRAY value via
*op->resvalue/resnull, set by ExecEvalJsonExprBehavior(), because
that's the ON EMPTY behavior specified in the constraint.  The bug was
that the code in ExecEvalJsonExprCoercion() failed to set val_string
to that value ("[]") before passing to InputFunctionCallSafe(), so the
latter would assume the input is NULL.

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-17 07:11  Anton A. Melnikov <a.melnikov@postgrespro.ru>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Anton A. Melnikov @ 2023-10-17 07:11 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: jian he <jian.universality@gmail.com>; Nikita Malakhov <hukutoc@gmail.com>; Andres Freund <andres@anarazel.de>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers


On 17.10.2023 07:02, Amit Langote wrote:

> One thing jian he missed during the debugging is that
> ExecEvalJsonExprCoersion() receives the EMPTY ARRAY value via
> *op->resvalue/resnull, set by ExecEvalJsonExprBehavior(), because
> that's the ON EMPTY behavior specified in the constraint.  The bug was
> that the code in ExecEvalJsonExprCoercion() failed to set val_string
> to that value ("[]") before passing to InputFunctionCallSafe(), so the
> latter would assume the input is NULL.
>
Thank a lot for this remark!

I tried to dig to the transformJsonOutput() to fix it earlier at the analyze stage,
but it looks like a rather hard way.

Maybe simple in accordance with you note remove the second condition from this line:
if (jb && JB_ROOT_IS_SCALAR(jb)) ?

There is a simplified reproduction before such a fix:
postgres=# select JSON_QUERY(jsonb '[]', '$' RETURNING char(5) OMIT QUOTES EMPTY ON EMPTY);
server closed the connection unexpectedly
         This probably means the server terminated abnormally

after:
postgres=# select JSON_QUERY(jsonb '[]', '$' RETURNING char(5) OMIT QUOTES EMPTY ON EMPTY);
  json_query
------------
  []
(1 row)

And at the moment i havn't found any side effects of that fix.
Please point me if i'm missing something.

With the best wishes!

-- 
Anton A. Melnikov
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-17 07:17  Amit Langote <amitlangote09@gmail.com>
  parent: Anton A. Melnikov <a.melnikov@postgrespro.ru>
  0 siblings, 0 replies; 300+ messages in thread

From: Amit Langote @ 2023-10-17 07:17 UTC (permalink / raw)
  To: Anton A. Melnikov <a.melnikov@postgrespro.ru>; +Cc: jian he <jian.universality@gmail.com>; Nikita Malakhov <hukutoc@gmail.com>; Andres Freund <andres@anarazel.de>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

Hi Anton,

On Tue, Oct 17, 2023 at 4:11 PM Anton A. Melnikov
<a.melnikov@postgrespro.ru> wrote:
> On 17.10.2023 07:02, Amit Langote wrote:
>
> > One thing jian he missed during the debugging is that
> > ExecEvalJsonExprCoersion() receives the EMPTY ARRAY value via
> > *op->resvalue/resnull, set by ExecEvalJsonExprBehavior(), because
> > that's the ON EMPTY behavior specified in the constraint.  The bug was
> > that the code in ExecEvalJsonExprCoercion() failed to set val_string
> > to that value ("[]") before passing to InputFunctionCallSafe(), so the
> > latter would assume the input is NULL.
> >
> Thank a lot for this remark!
>
> I tried to dig to the transformJsonOutput() to fix it earlier at the analyze stage,
> but it looks like a rather hard way.

Indeed.  As I said, the problem was a bug in ExecEvalJsonExprCoercion().

>
> Maybe simple in accordance with you note remove the second condition from this line:
> if (jb && JB_ROOT_IS_SCALAR(jb)) ?

Yeah, that's how I would fix it.

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-17 11:12  Amit Langote <amitlangote09@gmail.com>
  parent: Nikita Malakhov <hukutoc@gmail.com>
  1 sibling, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-10-17 11:12 UTC (permalink / raw)
  To: Nikita Malakhov <hukutoc@gmail.com>; +Cc: Andres Freund <andres@anarazel.de>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; jian he <jian.universality@gmail.com>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

On Mon, Oct 16, 2023 at 5:21 PM Nikita Malakhov <hukutoc@gmail.com> wrote:
>
> Hi!
>
> With the latest set of patches we encountered failure with the following query:
>
> postgres@postgres=# SELECT JSON_QUERY(jsonpath '"aaa"', '$' RETURNING text);
> server closed the connection unexpectedly
>         This probably means the server terminated abnormally
>         before or while processing the request.
> The connection to the server was lost. Attempting reset: Failed.
> The connection to the server was lost. Attempting reset: Failed.
> Time: 11.165 ms
>
> A colleague of mine, Anton Melnikov, proposed the following changes which slightly
> alter coercion functions to process this kind of error correctly.
>
> Please check attached patch set.

Thanks for the patches.

I think I understand patch 1. It makes each of JSON_{QUERY | VALUE |
EXISTS}() use FORMAT JSON for the context item by default, which I
think is the correct behavior.

As for patch 2, maybe the executor part is fine, but I'm not so sure
about the parser part.  Could you please explain why you think the
parser must check error-safety of the target type for allowing IO
coercion for non-ERROR behaviors?

Even if we consider that that's what should be done, it doesn't seem
like a good idea for the parser to implement its own logic for
determining error-safety.  IOW, the parser should really be using some
type cache API.  I thought there might have been a flag in pg_proc
(prosafe) or pg_type (typinsafe), but apparently there isn't.

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-18 02:19  jian he <jian.universality@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: jian he @ 2023-10-18 02:19 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: Nikita Malakhov <hukutoc@gmail.com>; Andres Freund <andres@anarazel.de>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

Hi.
based on v22.

I added some tests again json_value for the sake of coverager test.

A previous email thread mentioned needing to check *empty in ExecEvalJsonExpr.
since JSON_VALUE_OP, JSON_QUERY_OP, JSON_EXISTS_OP all need to have
*empty cases, So I refactored a little bit.
might be helpful. Maybe we can also refactor *error cases.

The following part is not easy to understand.
res = ExecPrepareJsonItemCoercion(jbv,
+  jsestate->item_jcstates,
+  &post_eval->jcstate);
+ if (post_eval->jcstate &&
+ post_eval->jcstate->coercion &&
+ (post_eval->jcstate->coercion->via_io ||
+ post_eval->jcstate->coercion->via_populate))

Attachments:

v22-0001-add-some-test-refactor-ExecEvalJsonExpr.patch (text/x-patch, 5.8K) download From 439f98ae55e6f67ca6c5bf023c6e9ab2796f4c49 Mon Sep 17 00:00:00 2001 From: pgaddict <jian.universality@gmail.com> Date: Wed, 18 Oct 2023 10:12:33 +0800 Subject: [PATCH v22 1/1] add some test, refactor ExecEvalJsonExpr add some tests again json_value for code coverage. refactor ExecEvalJsonExpr, let JSON_VALUE_OP, JSON_QUERY_OP, JSON_EXISTS_OP handle *empty in a common way. --- src/backend/executor/execExprInterp.c | 41 +++++++++------------ src/test/regress/expected/jsonb_sqljson.out | 36 ++++++++++++++++++ src/test/regress/sql/jsonb_sqljson.sql | 6 +++ 3 files changed, 60 insertions(+), 23 deletions(-) diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 123121a9..1a1f2089 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -4218,7 +4218,8 @@ ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) *op->resvalue = (Datum) 0; return; } - + if(*empty) + goto return_empty; resnull = false; res = BoolGetDatum(exists); break; @@ -4236,7 +4237,10 @@ ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) *op->resvalue = (Datum) 0; return; } + if (*empty) + goto return_empty; resnull = !DatumGetPointer(res); + break; case JSON_VALUE_OP: @@ -4253,15 +4257,14 @@ ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) *op->resvalue = (Datum) 0; return; } - - if (!jbv) /* NULL or empty */ + if (*empty) + goto return_empty; + if (!jbv) /* NULL */ { resnull = true; + res = (Datum) 0; break; } - - Assert(!*empty); - resnull = false; /* Coerce scalar item to the output type */ @@ -4322,31 +4325,23 @@ ExecEvalJsonExpr(ExprState *state, ExprEvalStep *op, ExprContext *econtext) return; } + *op->resvalue = res; + *op->resnull = resnull; + return; /* * If the ON EMPTY behavior is to cause an error, do so here. Other * behaviors will be handled in ExecEvalJsonExprBehavior(). */ - if (*empty) - { - Assert(jexpr->on_empty); /* it is not JSON_EXISTS */ - - if (jexpr->on_empty->btype == JSON_BEHAVIOR_ERROR) +return_empty: + Assert(jexpr->on_empty); + *op->resnull = true; + *op->resvalue = (Datum) 0; + if (jexpr->on_empty->btype == JSON_BEHAVIOR_ERROR && throw_error) { - if (!throw_error) - { - *op->resnull = true; - *op->resvalue = (Datum) 0; - return; - } - ereport(ERROR, (errcode(ERRCODE_NO_SQL_JSON_ITEM), errmsg("no SQL/JSON item"))); } - } - - *op->resvalue = res; - *op->resnull = resnull; } /* @@ -4467,7 +4462,7 @@ ExecEvalJsonExprCoercion(ExprState *state, ExprEvalStep *op, { Jsonb *jb = resnull ? NULL : DatumGetJsonbP(res); - if (jb && JB_ROOT_IS_SCALAR(jb)) + if (jb) { omit_quotes = true; val_string = JsonbUnquote(jb); diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out index a3ba44cf..0526ff00 100644 --- a/src/test/regress/expected/jsonb_sqljson.out +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -512,6 +512,42 @@ SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 + Tue Feb 20 18:34:56 2018 PST (1 row) +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamp '2018-02-21 12:34:56 +10' AS ts); + json_value +-------------------------- + Wed Feb 21 12:34:56 2018 +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamp(3) '2018-02-21 12:34:56.123456 +10' AS ts); + json_value +------------------------------ + Wed Feb 21 12:34:56.123 2018 +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz(3) '2018-02-21 12:34:56.123456 +10' AS ts); + json_value +---------------------------------- + Tue Feb 20 18:34:56.123 2018 PST +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING date '2018-02-21 12:34:56 +10' AS ts); + json_value +------------ + 02-21-2018 +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING time '2018-02-21 12:34:56 +10' AS ts); + json_value +------------ + 12:34:56 +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timetz '2018-02-21 12:34:56 +10' AS ts); + json_value +------------- + 12:34:56+10 +(1 row) + SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); json_value ------------------------------ diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql index ab73a011..e28be0a2 100644 --- a/src/test/regress/sql/jsonb_sqljson.sql +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -138,6 +138,12 @@ SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING poi -- Test timestamptz passing and output SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamp '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamp(3) '2018-02-21 12:34:56.123456 +10' AS ts); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz(3) '2018-02-21 12:34:56.123456 +10' AS ts); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING date '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING time '2018-02-21 12:34:56 +10' AS ts); +SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timetz '2018-02-21 12:34:56 +10' AS ts); SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz); SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp); SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json); -- 2.34.1
^ permalink raw reply [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-25 17:13  Nikita Malakhov <hukutoc@gmail.com>
  parent: jian he <jian.universality@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Nikita Malakhov @ 2023-10-25 17:13 UTC (permalink / raw)
  To: jian he <jian.universality@gmail.com>; +Cc: Amit Langote <amitlangote09@gmail.com>; Andres Freund <andres@anarazel.de>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

Hi!

Amit, on previous email, patch #2 - I agree that it is not the best idea to
introduce
new type of logic into the parser, so this logic could be moved to the
executor,
or removed at all. What do you think of these options?

On Wed, Oct 18, 2023 at 5:19 AM jian he <jian.universality@gmail.com> wrote:

> Hi.
> based on v22.
>
> I added some tests again json_value for the sake of coverager test.
>
> A previous email thread mentioned needing to check *empty in
> ExecEvalJsonExpr.
> since JSON_VALUE_OP, JSON_QUERY_OP, JSON_EXISTS_OP all need to have
> *empty cases, So I refactored a little bit.
> might be helpful. Maybe we can also refactor *error cases.
>
> The following part is not easy to understand.
> res = ExecPrepareJsonItemCoercion(jbv,
> +  jsestate->item_jcstates,
> +  &post_eval->jcstate);
> + if (post_eval->jcstate &&
> + post_eval->jcstate->coercion &&
> + (post_eval->jcstate->coercion->via_io ||
> + post_eval->jcstate->coercion->via_populate))
>


-- 
Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-26 02:32  Amit Langote <amitlangote09@gmail.com>
  parent: Nikita Malakhov <hukutoc@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Amit Langote @ 2023-10-26 02:32 UTC (permalink / raw)
  To: Nikita Malakhov <hukutoc@gmail.com>; +Cc: jian he <jian.universality@gmail.com>; Andres Freund <andres@anarazel.de>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

Hi Nikita,

On Thu, Oct 26, 2023 at 2:13 AM Nikita Malakhov <hukutoc@gmail.com> wrote:
> Amit, on previous email, patch #2 - I agree that it is not the best idea to introduce
> new type of logic into the parser, so this logic could be moved to the executor,
> or removed at all. What do you think of these options?

Yes maybe, though I'd first like to have a good answer to why is that
logic necessary at all.  Maybe you think it's better to emit an error
in the SQL/JSON layer of code than in the type input function if it's
unsafe?

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-26 12:19  Nikita Malakhov <hukutoc@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  0 siblings, 1 reply; 300+ messages in thread

From: Nikita Malakhov @ 2023-10-26 12:19 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: jian he <jian.universality@gmail.com>; Andres Freund <andres@anarazel.de>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

Hi,

The main goal was to correctly process invalid queries (as in examples
above).
I'm not sure this could be done in type input functions. I thought that some
coercions could be checked before evaluating expressions for saving reasons.

--
Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-26 12:53  Amit Langote <amitlangote09@gmail.com>
  parent: Nikita Malakhov <hukutoc@gmail.com>
  0 siblings, 2 replies; 300+ messages in thread

From: Amit Langote @ 2023-10-26 12:53 UTC (permalink / raw)
  To: Nikita Malakhov <hukutoc@gmail.com>; +Cc: jian he <jian.universality@gmail.com>; Andres Freund <andres@anarazel.de>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

Hi,

On Thu, Oct 26, 2023 at 9:20 PM Nikita Malakhov <hukutoc@gmail.com> wrote:
>
> Hi,
>
> The main goal was to correctly process invalid queries (as in examples above).
> I'm not sure this could be done in type input functions. I thought that some
> coercions could be checked before evaluating expressions for saving reasons.

I assume by "invalid" you mean queries specifying types in RETURNING
that don't support soft-error handling in their input function.
Adding a check makes sense but its implementation should include a
type cache interface to check whether a given type has error-safe
input handling, possibly as a separate patch.  IOW, the SQL/JSON patch
shouldn't really make a list of types to report as unsupported.

-- 
Thanks, Amit Langote
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-10-26 13:23  Nikita Malakhov <hukutoc@gmail.com>
  parent: Amit Langote <amitlangote09@gmail.com>
  1 sibling, 1 reply; 300+ messages in thread

From: Nikita Malakhov @ 2023-10-26 13:23 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: jian he <jian.universality@gmail.com>; Andres Freund <andres@anarazel.de>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

Hi,

Agreed on the latter, that must not be the part of it for sure.
Would think on how to make this part correct.

-- 
Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-11-03 15:20  Nikita Malakhov <hukutoc@gmail.com>
  parent: Nikita Malakhov <hukutoc@gmail.com>
  0 siblings, 0 replies; 300+ messages in thread

From: Nikita Malakhov @ 2023-11-03 15:20 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; +Cc: jian he <jian.universality@gmail.com>; Andres Freund <andres@anarazel.de>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; Erik Rijkers <er@xs4all.nl>; pgsql-hackers

Hi!

Read Tom Lane's note in previous discussion (quite long, so I've missed it)
on pg_proc column -

>I strongly recommend against having a new pg_proc column at all.
>I doubt that you really need it, and having one will create
>enormous mechanical burdens to making the conversion.  (For example,
>needing a catversion bump every time we convert one more function,
>or an extension version bump to convert extensions.)

so should figure out another way to do it.

Regards,
--
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-11-11 02:52  Erik Rijkers <er@xs4all.nl>
  parent: Amit Langote <amitlangote09@gmail.com>
  1 sibling, 1 reply; 300+ messages in thread

From: Erik Rijkers @ 2023-11-11 02:52 UTC (permalink / raw)
  To: Amit Langote <amitlangote09@gmail.com>; Nikita Malakhov <hukutoc@gmail.com>; +Cc: jian he <jian.universality@gmail.com>; Andres Freund <andres@anarazel.de>; Alvaro Herrera <alvherre@alvh.no-ip.org>; Andrew Dunstan <andrew@dunslane.net>; pgsql-hackers

Hi,

At the moment, what is the patchset to be tested?  The latest SQL/JSON 
server I have is from September, and it's become unclear to me what 
belongs to the SQL/JSON patchset.  It seems to me cfbot erroneously 
shows green because it successfully compiles later detail-patches (i.e., 
not the SQL/JSON set itself). Please correct me if I'm wrong and it is 
in fact possible to derive from cfbot a patchset that are the ones to 
use to build the latest SQL/JSON server.

Thanks!

Erik





^ permalink  raw  reply  [nested|flat] 300+ messages in thread

* Re: remaining sql/json patches
@ 2023-11-11 02:56  Amit Langote <amitlangote09@gmail.com>
  parent: Erik Rijkers <er@xs4all.nl>
  0 siblings, 0 replies; 300+ messages in thread

From: Amit Langote @ 2023-11-11 02:56 UTC (permalink / raw)
  To: Erik Rijkers <er@xs4all.nl>; +Cc: Alvaro Herrera <alvherre@alvh.no-ip.org>; Andres Freund <andres@anarazel.de>; Andrew Dunstan <andrew@dunslane.net>; Nikita Malakhov <hukutoc@gmail.com>; pgsql-hackers; jian he <jian.universality@gmail.com>

Hi Erik,

On Sat, Nov 11, 2023 at 11:52 Erik Rijkers <er@xs4all.nl> wrote:

> Hi,
>
> At the moment, what is the patchset to be tested?  The latest SQL/JSON
> server I have is from September, and it's become unclear to me what
> belongs to the SQL/JSON patchset.  It seems to me cfbot erroneously
> shows green because it successfully compiles later detail-patches (i.e.,
> not the SQL/JSON set itself). Please correct me if I'm wrong and it is
> in fact possible to derive from cfbot a patchset that are the ones to
> use to build the latest SQL/JSON server.


I’ll be posting a new set that addresses Andres’ comments early next week.

>

^ permalink  raw  reply  [nested|flat] 300+ messages in thread

217 further messages in this thread omitted from this page (render size limit).
Use the per-message pages, or the t.mbox.gz byte stream, for the complete thread.


end of thread, other threads:[~2024-11-14 06:19 UTC | newest]

Thread overview: 300+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2023-06-19 08:31 remaining sql/json patches Amit Langote <amitlangote09@gmail.com>
2023-06-21 08:25 ` Amit Langote <amitlangote09@gmail.com>
2023-07-07 11:30   ` Peter Eisentraut <peter@eisentraut.org>
2023-07-07 11:59     ` Amit Langote <amitlangote09@gmail.com>
2023-07-07 12:19       ` Amit Langote <amitlangote09@gmail.com>
2023-07-07 12:28   ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-07-10 12:56     ` Amit Langote <amitlangote09@gmail.com>
2023-07-10 14:47       ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-07-10 14:51         ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-07-12 09:41           ` Amit Langote <amitlangote09@gmail.com>
2023-07-12 13:23             ` Amit Langote <amitlangote09@gmail.com>
2023-07-13 03:47               ` Amit Langote <amitlangote09@gmail.com>
2023-07-13 16:54                 ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-07-14 07:13                   ` Amit Langote <amitlangote09@gmail.com>
2023-07-17 05:00                     ` jian he <jian.universality@gmail.com>
2023-07-17 07:15                       ` Erik Rijkers <er@xs4all.nl>
2023-07-18 09:11                         ` Amit Langote <amitlangote09@gmail.com>
2023-07-18 15:53                           ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-07-19 08:17                             ` Amit Langote <amitlangote09@gmail.com>
2023-07-19 12:46                               ` Amit Langote <amitlangote09@gmail.com>
2023-07-20 08:19                               ` Amit Langote <amitlangote09@gmail.com>
2023-07-20 15:48                                 ` Amit Langote <amitlangote09@gmail.com>
2023-07-20 16:02                                   ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-07-21 10:33                                     ` Amit Langote <amitlangote09@gmail.com>
2023-07-23 08:17                                       ` jian he <jian.universality@gmail.com>
2023-08-04 12:21                                         ` Amit Langote <amitlangote09@gmail.com>
2023-08-15 08:58                                           ` jian he <jian.universality@gmail.com>
2023-08-16 04:27                                             ` Amit Langote <amitlangote09@gmail.com>
2023-08-31 12:57                                               ` Amit Langote <amitlangote09@gmail.com>
2023-08-31 13:51                                                 ` Erik Rijkers <er@xs4all.nl>
2023-09-01 04:52                                                   ` Amit Langote <amitlangote09@gmail.com>
2023-09-06 15:01                                                     ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-09-12 07:52                                                       ` Peter Eisentraut <peter@eisentraut.org>
2023-09-12 14:43                                                         ` Tom Lane <tgl@sss.pgh.pa.us>
2023-09-14 08:14                                                       ` Amit Langote <amitlangote09@gmail.com>
2023-09-17 06:37                                                         ` Erik Rijkers <er@xs4all.nl>
2023-09-18 03:15                                                           ` Amit Langote <amitlangote09@gmail.com>
2023-09-18 10:12                                                             ` Erik Rijkers <er@xs4all.nl>
2023-09-18 10:20                                                               ` Amit Langote <amitlangote09@gmail.com>
2023-09-18 11:14                                                                 ` Erik Rijkers <er@xs4all.nl>
2023-09-18 11:53                                                                   ` Erik Rijkers <er@xs4all.nl>
2023-09-19 10:37                                                                     ` jian he <jian.universality@gmail.com>
2023-09-19 12:00                                                                       ` Amit Langote <amitlangote09@gmail.com>
2023-09-20 03:07                                                                         ` Amit Langote <amitlangote09@gmail.com>
2023-09-20 19:14                                                                           ` Andrew Dunstan <andrew@dunslane.net>
2023-09-21 08:32                                                                             ` Amit Langote <amitlangote09@gmail.com>
2023-09-21 08:57                                                                               ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-09-21 12:41                                                                                 ` Amit Langote <amitlangote09@gmail.com>
2023-09-27 13:55                                                                                   ` Amit Langote <amitlangote09@gmail.com>
2023-09-27 14:23                                                                                     ` Erik Rijkers <er@xs4all.nl>
2023-09-28 04:35                                                                                       ` Amit Langote <amitlangote09@gmail.com>
2023-09-28 11:04                                                                                     ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-09-29 04:57                                                                                       ` Amit Langote <amitlangote09@gmail.com>
2023-10-02 04:24                                                                                         ` Amit Langote <amitlangote09@gmail.com>
2023-10-02 05:26                                                                                           ` Amit Langote <amitlangote09@gmail.com>
2023-10-03 13:11                                                                                             ` Amit Langote <amitlangote09@gmail.com>
2023-10-04 13:26                                                                                               ` Amit Langote <amitlangote09@gmail.com>
2023-10-06 09:23                                                                                                 ` Amit Langote <amitlangote09@gmail.com>
2023-10-06 10:01                                                                                                   ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-10-06 11:27                                                                                                     ` Amit Langote <amitlangote09@gmail.com>
2023-10-06 21:48                                                                                         ` Andres Freund <andres@anarazel.de>
2023-10-07 13:54                                                                                           ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-10-11 05:08                                                                                           ` Amit Langote <amitlangote09@gmail.com>
2023-10-11 12:34                                                                                             ` Amit Langote <amitlangote09@gmail.com>
2023-10-16 08:20                                                                                               ` Nikita Malakhov <hukutoc@gmail.com>
2023-10-16 08:33                                                                                                 ` Nikita Malakhov <hukutoc@gmail.com>
2023-10-16 09:47                                                                                                   ` Amit Langote <amitlangote09@gmail.com>
2023-10-16 09:59                                                                                                     ` Nikita Malakhov <hukutoc@gmail.com>
2023-10-16 12:49                                                                                                     ` jian he <jian.universality@gmail.com>
2023-10-16 13:44                                                                                                       ` Anton A. Melnikov <a.melnikov@postgrespro.ru>
2023-10-17 04:02                                                                                                         ` Amit Langote <amitlangote09@gmail.com>
2023-10-17 07:11                                                                                                           ` Anton A. Melnikov <a.melnikov@postgrespro.ru>
2023-10-17 07:17                                                                                                             ` Amit Langote <amitlangote09@gmail.com>
2023-10-17 11:12                                                                                                 ` Amit Langote <amitlangote09@gmail.com>
2023-10-18 02:19                                                                                                   ` jian he <jian.universality@gmail.com>
2023-10-25 17:13                                                                                                     ` Nikita Malakhov <hukutoc@gmail.com>
2023-10-26 02:32                                                                                                       ` Amit Langote <amitlangote09@gmail.com>
2023-10-26 12:19                                                                                                         ` Nikita Malakhov <hukutoc@gmail.com>
2023-10-26 12:53                                                                                                           ` Amit Langote <amitlangote09@gmail.com>
2023-10-26 13:23                                                                                                             ` Nikita Malakhov <hukutoc@gmail.com>
2023-11-01 12:00                                                                                                               ` Nikita Malakhov <hukutoc@gmail.com>
2023-11-03 15:20                                                                                                                 ` Nikita Malakhov <hukutoc@gmail.com>
2023-11-11 02:52                                                                                                             ` Erik Rijkers <er@xs4all.nl>
2023-11-11 02:56                                                                                                               ` Amit Langote <amitlangote09@gmail.com>
2023-11-15 13:00                                                                                           ` Amit Langote <amitlangote09@gmail.com>
2023-11-15 17:11                                                                                             ` Andres Freund <andres@anarazel.de>
2023-11-15 17:12                                                                                               ` Andres Freund <andres@anarazel.de>
2023-11-16 08:48                                                                                               ` Amit Langote <amitlangote09@gmail.com>
2023-11-17 07:27                                                                                                 ` jian he <jian.universality@gmail.com>
2023-11-17 09:17                                                                                                   ` Amit Langote <amitlangote09@gmail.com>
2023-11-17 09:40                                                                                                     ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-11-22 08:37                                                                                                       ` Amit Langote <amitlangote09@gmail.com>
2023-11-21 03:52                                                                                                 ` Amit Langote <amitlangote09@gmail.com>
2023-11-22 19:38                                                                                                   ` Andres Freund <andres@anarazel.de>
2023-11-23 06:55                                                                                                     ` jian he <jian.universality@gmail.com>
2023-11-23 10:46                                                                                                       ` jian he <jian.universality@gmail.com>
2023-11-24 08:41                                                                                                         ` jian he <jian.universality@gmail.com>
2023-11-28 02:00                                                                                                         ` jian he <jian.universality@gmail.com>
2023-12-05 12:25                                                                                                           ` Amit Langote <amitlangote09@gmail.com>
2023-12-06 10:42                                                                                                             ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-12-06 14:02                                                                                                               ` Amit Langote <amitlangote09@gmail.com>
2023-12-06 15:26                                                                                                                 ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-12-07 09:32                                                                                                                   ` Amit Langote <amitlangote09@gmail.com>
2023-12-07 10:36                                                                                                                     ` Erik Rijkers <er@xs4all.nl>
2023-12-07 10:51                                                                                                                       ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-12-07 12:07                                                                                                                         ` Amit Langote <amitlangote09@gmail.com>
2023-12-07 18:41                                                                                                                           ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-12-08 10:34                                                                                                                             ` Amit Langote <amitlangote09@gmail.com>
2023-12-09 05:05                                                                                                                               ` jian he <jian.universality@gmail.com>
2023-12-13 09:59                                                                                                                                 ` jian he <jian.universality@gmail.com>
2023-12-14 08:10                                                                                                                                   ` Amit Langote <amitlangote09@gmail.com>
2023-12-15 08:36                                                                                                                                     ` jian he <jian.universality@gmail.com>
2023-12-18 09:45                                                                                                                                       ` jian he <jian.universality@gmail.com>
2023-12-08 17:30                                                                                                                           ` Andres Freund <andres@anarazel.de>
2023-12-14 08:04                                                                                                                             ` Amit Langote <amitlangote09@gmail.com>
2023-12-20 08:36                                                                                                                               ` Amit Langote <amitlangote09@gmail.com>
2023-12-22 13:01                                                                                                                                 ` jian he <jian.universality@gmail.com>
2023-12-25 05:03                                                                                                                                   ` jian he <jian.universality@gmail.com>
2024-01-03 10:50                                                                                                                                   ` jian he <jian.universality@gmail.com>
2024-01-03 10:53                                                                                                                                     ` jian he <jian.universality@gmail.com>
2024-01-06 00:44                                                                                                                                     ` jian he <jian.universality@gmail.com>
2024-01-08 00:00                                                                                                                                       ` jian he <jian.universality@gmail.com>
2024-01-16 10:00                                                                                                                                         ` Amit Langote <amitlangote09@gmail.com>
2024-01-18 13:12                                                                                                                                   ` Amit Langote <amitlangote09@gmail.com>
2024-01-18 13:35                                                                                                                                     ` Amit Langote <amitlangote09@gmail.com>
2024-01-18 11:46                                                                                                                                 ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2024-01-18 17:11                                                                                                                                   ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2024-01-19 03:09                                                                                                                                     ` Amit Langote <amitlangote09@gmail.com>
2024-01-19 10:46                                                                                                                                       ` jian he <jian.universality@gmail.com>
2024-01-22 00:00                                                                                                                                         ` jian he <jian.universality@gmail.com>
2024-01-22 05:55                                                                                                                                           ` Peter Smith <smithpb2250@gmail.com>
2024-01-22 06:14                                                                                                                                             ` jian he <jian.universality@gmail.com>
2024-01-22 14:27                                                                                                                                         ` Amit Langote <amitlangote09@gmail.com>
2024-01-22 15:46                                                                                                                                           ` jian he <jian.universality@gmail.com>
2024-01-23 08:51                                                                                                                                             ` jian he <jian.universality@gmail.com>
2024-01-22 16:19                                                                                                                                     ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2024-01-23 13:46                                                                                                                                       ` Amit Langote <amitlangote09@gmail.com>
2024-01-24 13:11                                                                                                                                         ` Amit Langote <amitlangote09@gmail.com>
2024-01-25 09:09                                                                                                                                           ` Amit Langote <amitlangote09@gmail.com>
2024-01-25 11:54                                                                                                                                             ` Amit Langote <amitlangote09@gmail.com>
2024-01-25 14:39                                                                                                                                               ` jian he <jian.universality@gmail.com>
2024-01-31 14:51                                                                                                                                                 ` jian he <jian.universality@gmail.com>
2024-02-05 12:27                                                                                                                                                   ` jian he <jian.universality@gmail.com>
2024-02-06 04:55                                                                                                                                                 ` jian he <jian.universality@gmail.com>
2024-02-14 00:00                                                                                                                                                   ` jian he <jian.universality@gmail.com>
2024-03-04 09:40                                                                                                                                                     ` Amit Langote <amitlangote09@gmail.com>
2024-03-04 14:51                                                                                                                                                       ` Erik Rijkers <er@xs4all.nl>
2024-03-04 15:03                                                                                                                                                         ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2024-03-05 01:21                                                                                                                                                           ` Amit Langote <amitlangote09@gmail.com>
2024-03-05 04:28                                                                                                                                                             ` Andy Fan <zhihuifan1213@163.com>
2024-03-07 04:08                                                                                                                                                               ` jian he <jian.universality@gmail.com>
2024-03-09 23:12                                                                                                                                                                 ` Andy Fan <zhihuifan1213@163.com>
2024-03-05 04:42                                                                                                                                                             ` jian he <jian.universality@gmail.com>
2024-03-05 21:30                                                                                                                                                             ` Tomas Vondra <tomas.vondra@enterprisedb.com>
2024-03-06 04:07                                                                                                                                                               ` Amit Langote <amitlangote09@gmail.com>
2024-03-06 13:22                                                                                                                                                                 ` jian he <jian.universality@gmail.com>
2024-03-07 02:50                                                                                                                                                                   ` jian he <jian.universality@gmail.com>
2024-03-07 05:16                                                                                                                                                                     ` jian he <jian.universality@gmail.com>
2024-03-07 07:26                                                                                                                                                                 ` Amit Langote <amitlangote09@gmail.com>
2024-03-07 11:02                                                                                                                                                                   ` Tomas Vondra <tomas.vondra@enterprisedb.com>
2024-03-07 14:04                                                                                                                                                                   ` Tomas Vondra <tomas.vondra@enterprisedb.com>
2024-03-07 14:14                                                                                                                                                                     ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2024-03-07 14:23                                                                                                                                                                       ` Amit Langote <amitlangote09@gmail.com>
2024-03-08 04:04                                                                                                                                                                         ` jian he <jian.universality@gmail.com>
2024-03-10 14:57                                                                                                                                                                           ` jian he <jian.universality@gmail.com>
2024-03-11 03:30                                                                                                                                                                             ` jian he <jian.universality@gmail.com>
2024-03-11 07:45                                                                                                                                                                               ` jian he <jian.universality@gmail.com>
2024-03-11 09:20                                                                                                                                                                                 ` jian he <jian.universality@gmail.com>
2024-03-11 14:55                                                                                                                                                                                   ` Shruthi Gowda <gowdashru@gmail.com>
2024-03-11 15:34                                                                                                                                                                                     ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2024-03-11 19:07                                                                                                                                                                                       ` Shruthi Gowda <gowdashru@gmail.com>
2024-03-12 09:41                                                                                                                                                                                         ` Himanshu Upadhyaya <upadhyaya.himanshu@gmail.com>
2024-03-12 12:07                                                                                                                                                                                           ` Amit Langote <amitlangote09@gmail.com>
2024-03-13 04:19                                                                                                                                                                                             ` Himanshu Upadhyaya <upadhyaya.himanshu@gmail.com>
2024-03-15 10:30                                                                                                                                                                               ` jian he <jian.universality@gmail.com>
2024-03-18 07:56                                                                                                                                                                                 ` Himanshu Upadhyaya <upadhyaya.himanshu@gmail.com>
2024-03-18 10:03                                                                                                                                                                                   ` Amit Langote <amitlangote09@gmail.com>
2024-03-19 04:03                                                                                                                                                                                     ` Himanshu Upadhyaya <upadhyaya.himanshu@gmail.com>
2024-03-12 20:47                                                                                                                                                                   ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2024-03-14 10:12                                                                                                                                                                     ` jian he <jian.universality@gmail.com>
2024-03-18 05:00                                                                                                                                                                     ` Amit Langote <amitlangote09@gmail.com>
2024-03-06 11:58                                                                                                                                                             ` Himanshu Upadhyaya <upadhyaya.himanshu@gmail.com>
2024-03-06 15:34                                                                                                                                                               ` Tomas Vondra <tomas.vondra@enterprisedb.com>
2024-03-07 05:18                                                                                                                                                                 ` Himanshu Upadhyaya <upadhyaya.himanshu@gmail.com>
2024-03-07 11:13                                                                                                                                                                   ` Tomas Vondra <tomas.vondra@enterprisedb.com>
2024-03-07 12:06                                                                                                                                                                     ` Amit Langote <amitlangote09@gmail.com>
2024-03-07 13:46                                                                                                                                                                       ` jian he <jian.universality@gmail.com>
2024-03-07 14:11                                                                                                                                                                         ` Amit Langote <amitlangote09@gmail.com>
2024-03-19 10:45                                                                                                                                                                       ` Amit Langote <amitlangote09@gmail.com>
2024-03-20 03:41                                                                                                                                                                         ` jian he <jian.universality@gmail.com>
2024-03-20 06:51                                                                                                                                                                           ` jian he <jian.universality@gmail.com>
2024-03-20 11:46                                                                                                                                                                             ` jian he <jian.universality@gmail.com>
2024-03-20 12:53                                                                                                                                                                               ` Amit Langote <amitlangote09@gmail.com>
2024-03-21 16:08                                                                                                                                                                                 ` Amit Langote <amitlangote09@gmail.com>
2024-03-26 10:16                                                                                                                                                                                   ` jian he <jian.universality@gmail.com>
2024-03-27 03:41                                                                                                                                                                                     ` jian he <jian.universality@gmail.com>
2024-03-27 04:34                                                                                                                                                                                       ` Amit Langote <amitlangote09@gmail.com>
2024-03-28 05:23                                                                                                                                                                                         ` Amit Langote <amitlangote09@gmail.com>
2024-03-28 17:04                                                                                                                                                                                           ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2024-03-29 14:03                                                                                                                                                                                             ` Amit Langote <amitlangote09@gmail.com>
2024-04-01 00:00                                                                                                                                                                                               ` jian he <jian.universality@gmail.com>
2024-04-01 03:47                                                                                                                                                                                                 ` jian he <jian.universality@gmail.com>
2024-04-01 08:56                                                                                                                                                                                                 ` jian he <jian.universality@gmail.com>
2024-04-02 06:54                                                                                                                                                                                                   ` jian he <jian.universality@gmail.com>
2024-04-02 13:57                                                                                                                                                                                                 ` Amit Langote <amitlangote09@gmail.com>
2024-04-03 03:30                                                                                                                                                                                                   ` jian he <jian.universality@gmail.com>
2024-04-03 07:15                                                                                                                                                                                                     ` jian he <jian.universality@gmail.com>
2024-04-03 09:36                                                                                                                                                                                                       ` jian he <jian.universality@gmail.com>
2024-04-03 12:38                                                                                                                                                                                                       ` Amit Langote <amitlangote09@gmail.com>
2024-04-03 14:48                                                                                                                                                                                                         ` jian he <jian.universality@gmail.com>
2024-04-04 12:02                                                                                                                                                                                                           ` Amit Langote <amitlangote09@gmail.com>
2024-04-05 06:00                                                                                                                                                                                                             ` Alexander Lakhin <exclusion@gmail.com>
2024-04-05 06:07                                                                                                                                                                                                               ` Michael Paquier <michael@paquier.xyz>
2024-04-06 05:00                                                                                                                                                                                                                 ` Amit Langote <amitlangote09@gmail.com>
2024-04-05 07:09                                                                                                                                                                                                               ` Amit Langote <amitlangote09@gmail.com>
2024-04-05 08:00                                                                                                                                                                                                                 ` Alexander Lakhin <exclusion@gmail.com>
2024-04-05 08:09                                                                                                                                                                                                                   ` Amit Langote <amitlangote09@gmail.com>
2024-04-05 12:34                                                                                                                                                                                                             ` Amit Langote <amitlangote09@gmail.com>
2024-04-06 03:31                                                                                                                                                                                                               ` jian he <jian.universality@gmail.com>
2024-04-06 06:03                                                                                                                                                                                                                 ` Amit Langote <amitlangote09@gmail.com>
2024-04-06 06:55                                                                                                                                                                                                                   ` jian he <jian.universality@gmail.com>
2024-04-06 15:10                                                                                                                                                                                                                     ` Amit Langote <amitlangote09@gmail.com>
2024-04-07 04:30                                                                                                                                                                                                                       ` jian he <jian.universality@gmail.com>
2024-04-07 13:20                                                                                                                                                                                                                         ` jian he <jian.universality@gmail.com>
2024-04-07 13:36                                                                                                                                                                                                                           ` Amit Langote <amitlangote09@gmail.com>
2024-04-07 16:34                                                                                                                                                                                                                             ` jian he <jian.universality@gmail.com>
2024-04-08 03:21                                                                                                                                                                                                                               ` jian he <jian.universality@gmail.com>
2024-04-08 05:01                                                                                                                                                                                                                                 ` jian he <jian.universality@gmail.com>
2024-04-08 09:08                                                                                                                                                                                                                                   ` Amit Langote <amitlangote09@gmail.com>
2024-05-15 23:49                                                                                                                                                                                                                                     ` Thom Brown <thom@linux.com>
2024-05-20 11:51                                                                                                                                                                                                                                       ` Amit Langote <amitlangote09@gmail.com>
2024-05-28 00:00                                                                                                                                                                                                                                         ` jian he <jian.universality@gmail.com>
2024-06-26 11:00                                                                                                                                                                                                                                           ` Alexander Lakhin <exclusion@gmail.com>
2024-06-28 06:15                                                                                                                                                                                                                                             ` Amit Langote <amitlangote09@gmail.com>
2024-06-28 08:00                                                                                                                                                                                                                                               ` Alexander Lakhin <exclusion@gmail.com>
2024-06-28 13:43                                                                                                                                                                                                                                                 ` Amit Langote <amitlangote09@gmail.com>
2024-04-06 14:34                                                                                                                                                                                                               ` jian he <jian.universality@gmail.com>
2024-04-04 06:41                                                                                                                                                                                                         ` jian he <jian.universality@gmail.com>
2024-04-04 07:50                                                                                                                                                                                                           ` jian he <jian.universality@gmail.com>
2024-04-04 08:24                                                                                                                                                                                                             ` jian he <jian.universality@gmail.com>
2024-03-29 03:20                                                                                                                                                                                           ` jian he <jian.universality@gmail.com>
2024-03-29 09:59                                                                                                                                                                                             ` jian he <jian.universality@gmail.com>
2024-03-29 14:01                                                                                                                                                                                               ` Amit Langote <amitlangote09@gmail.com>
2024-04-02 13:26                                                                                                                                                                                   ` jian he <jian.universality@gmail.com>
2024-03-22 00:51                                                                                                                                                                                 ` Kyotaro Horiguchi <horikyota.ntt@gmail.com>
2024-03-22 02:44                                                                                                                                                                                   ` Amit Langote <amitlangote09@gmail.com>
2024-03-22 04:40                                                                                                                                                                                     ` Kyotaro Horiguchi <horikyota.ntt@gmail.com>
2024-01-25 09:20                                                                                                                                           ` jian he <jian.universality@gmail.com>
2023-12-07 17:19                                                                                                                   ` Robert Haas <robertmhaas@gmail.com>
2023-12-08 06:59                                                                                                                     ` Amit Langote <amitlangote09@gmail.com>
2023-12-08 16:37                                                                                                                       ` Robert Haas <robertmhaas@gmail.com>
2023-12-08 17:05                                                                                                                         ` Andrew Dunstan <andrew@dunslane.net>
2023-12-14 08:05                                                                                                                           ` Amit Langote <amitlangote09@gmail.com>
2023-12-07 03:10                                                                                                                 ` jian he <jian.universality@gmail.com>
2023-12-07 10:39                                                                                                                   ` jian he <jian.universality@gmail.com>
2023-12-07 12:13                                                                                                                     ` Amit Langote <amitlangote09@gmail.com>
2023-12-07 08:25                                                                                                                 ` Peter Eisentraut <peter@eisentraut.org>
2023-11-24 09:32                                                                                                     ` Amit Langote <amitlangote09@gmail.com>
2023-11-24 12:27                                                                                                       ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-11-27 10:09                                                                                                         ` Amit Langote <amitlangote09@gmail.com>
2023-11-27 10:42                                                                                                           ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-11-27 13:56                                                                                                             ` Andrew Dunstan <andrew@dunslane.net>
2023-11-27 14:06                                                                                                               ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-11-27 17:50                                                                                                                 ` Andres Freund <andres@anarazel.de>
2024-01-22 06:19                                                                                                                 ` John Naylor <johncnaylorls@gmail.com>
2023-11-28 05:10                                                                                                               ` John Naylor <johncnaylorls@gmail.com>
2023-11-28 20:49                                                                                                                 ` Andrew Dunstan <andrew@dunslane.net>
2023-11-28 20:57                                                                                                                   ` Andrew Dunstan <andrew@dunslane.net>
2023-11-29 00:08                                                                                                                     ` Andres Freund <andres@anarazel.de>
2023-11-29 00:32                                                                                                                   ` Tom Lane <tgl@sss.pgh.pa.us>
2023-11-29 01:58                                                                                                                     ` Andrew Dunstan <andrew@dunslane.net>
2023-11-29 02:10                                                                                                                       ` Andres Freund <andres@anarazel.de>
2023-11-29 12:37                                                                                                                         ` Andrew Dunstan <andrew@dunslane.net>
2023-11-29 17:42                                                                                                                           ` Andres Freund <andres@anarazel.de>
2023-11-29 19:21                                                                                                                             ` Andrew Dunstan <andrew@dunslane.net>
2023-11-29 19:41                                                                                                                               ` Andres Freund <andres@anarazel.de>
2023-11-29 20:24                                                                                                                                 ` Andrew Dunstan <andrew@dunslane.net>
2023-11-29 17:03                                                                                                                       ` Andrew Dunstan <andrew@dunslane.net>
2023-11-16 04:53                                                                                             ` Erik Rijkers <er@xs4all.nl>
2023-11-16 04:57                                                                                               ` Amit Langote <amitlangote09@gmail.com>
2023-11-16 06:53                                                                                                 ` Amit Langote <amitlangote09@gmail.com>
2023-11-21 07:09                                                                                                   ` Peter Eisentraut <peter@eisentraut.org>
2023-11-22 06:09                                                                                                     ` Amit Langote <amitlangote09@gmail.com>
2023-11-22 07:37                                                                                                       ` Andres Freund <andres@anarazel.de>
2023-11-22 08:16                                                                                                         ` Amit Langote <amitlangote09@gmail.com>
2023-11-22 13:23                                                                                                       ` Amit Langote <amitlangote09@gmail.com>
2024-11-09 08:22                                                                             ` Nikita Malakhov <hukutoc@gmail.com>
2024-11-11 00:28                                                                               ` Amit Langote <amitlangote09@gmail.com>
2024-11-14 06:19                                                                                 ` Nikita Malakhov <hukutoc@gmail.com>
2023-09-19 10:18                                                             ` Alvaro Herrera <alvherre@alvh.no-ip.org>
2023-09-19 11:56                                                               ` Amit Langote <amitlangote09@gmail.com>
2023-09-19 12:34                                                                 ` Erik Rijkers <er@xs4all.nl>
2023-09-19 12:51                                                                   ` Amit Langote <amitlangote09@gmail.com>
2023-07-26 08:10                                       ` Amit Langote <amitlangote09@gmail.com>
2023-07-27 09:36                                         ` Shinoda, Noriyoshi (HPE Services Japan - FSIP) <noriyoshi.shinoda@hpe.com>
2023-07-28 06:57                                           ` Amit Langote <amitlangote09@gmail.com>
2023-08-04 10:02                                       ` Erik Rijkers <er@xs4all.nl>
2023-08-04 10:05                                         ` Amit Langote <amitlangote09@gmail.com>
2023-07-20 01:35                           ` jian he <jian.universality@gmail.com>
2023-07-20 07:03                             ` Amit Langote <amitlangote09@gmail.com>

This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox