diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 2e64cc4..8d5bbd8 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -10324,50 +10324,53 @@ SELECT xml_is_well_formed_document(' Processing XML - - XPath - - To process values of data type xml, PostgreSQL offers - the functions xpath and + the functions xpath, xpath_exists, which evaluate XPath 1.0 - expressions. + expressions, and the XMLTABLE predicate. + + <literal>xpath</literal> + + + XPath + + xpath(xpath, xml , nsarray) - - The function xpath evaluates the XPath - expression xpath (a text value) - against the XML value - xml. It returns an array of XML values - corresponding to the node set produced by the XPath expression. - If the XPath expression returns a scalar value rather than a node set, - a single-element array is returned. - + + The function xpath evaluates the XPath + expression xpath (a text value) + against the XML value + xml. It returns an array of XML values + corresponding to the node set produced by the XPath expression. + If the XPath expression returns a scalar value rather than a node set, + a single-element array is returned. + - - The second argument must be a well formed XML document. In particular, - it must have a single root node element. - + + The second argument must be a well formed XML document. In particular, + it must have a single root node element. + - - The optional third argument of the function is an array of namespace - mappings. This array should be a two-dimensional text array with - the length of the second axis being equal to 2 (i.e., it should be an - array of arrays, each of which consists of exactly 2 elements). - The first element of each array entry is the namespace name (alias), the - second the namespace URI. It is not required that aliases provided in - this array be the same as those being used in the XML document itself (in - other words, both in the XML document and in the xpath - function context, aliases are local). - + + The optional third argument of the function is an array of namespace + mappings. This array should be a two-dimensional text array with + the length of the second axis being equal to 2 (i.e., it should be an + array of arrays, each of which consists of exactly 2 elements). + The first element of each array entry is the namespace name (alias), the + second the namespace URI. It is not required that aliases provided in + this array be the same as those being used in the XML document itself (in + other words, both in the XML document and in the xpath + function context, aliases are local). + - - Example: + + Example: test', ARRAY[ARRAY['my', 'http://example.com']]); @@ -10377,10 +10380,10 @@ SELECT xpath('/my:a/text()', 'test', {test} (1 row) ]]> - + - - To deal with default (anonymous) namespaces, do something like this: + + To deal with default (anonymous) namespaces, do something like this: test', ARRAY[ARRAY['mydefns', 'http://example.com']]); @@ -10390,27 +10393,31 @@ SELECT xpath('//mydefns:b/text()', 'test - + + - - xpath_exists - + + <literal>xpath_exists</literal> + + + xpath_exists + xpath_exists(xpath, xml , nsarray) - - The function xpath_exists is a specialized form - of the xpath function. Instead of returning the - individual XML values that satisfy the XPath, this function returns a - Boolean indicating whether the query was satisfied or not. This - function is equivalent to the standard XMLEXISTS predicate, - except that it also offers support for a namespace mapping argument. - + + The function xpath_exists is a specialized form + of the xpath function. Instead of returning the + individual XML values that satisfy the XPath, this function returns a + Boolean indicating whether the query was satisfied or not. This + function is equivalent to the standard XMLEXISTS predicate, + except that it also offers support for a namespace mapping argument. + - - Example: + + Example: test', ARRAY[ARRAY['my', 'http://example.com']]); @@ -10420,7 +10427,278 @@ SELECT xpath_exists('/my:a/text()', 'test + + + + + <literal>xmltable</literal> + + + xmltable + + + +xmltable( + xmlnamespaces(namespace uri AS namespace name|DEFAULT namespace uri , ...) + rowexpr PASSING BY REF xml BY REF + COLUMNS name type + PATH columnexpr + DEFAULT expr + NOT NULL | NULL , ... +) + + + + The xmltable produces a table based on the + passed XML value, an xpath filter to extract rows, and an + optional set of column definitions. + + + + For XML document: + + + AU + Australia + 3 + + + CN + China + 3 + + + HK + HongKong + 3 + + + IN + India + 3 + + + JP + Japan + 3Sinzo Abe + + + SG + Singapore + 3791 + + +]]> + + the following query produces the result: + + + + + + The optional xmlnamespaces clause is a comma-separated list of + namespaces of the form namespace-URI AS + namespace-name. It specifies the XML namespaces used in + the document and their aliases. The default namespace is specified with + DEFAULT namespace-URI. + +10' + COLUMNS a int PATH 'a'); + + a +---- + 10 +]]> + + + The required rowexpr argument is an xpath + expression that is evaluated against the supplied XML document to + obtain an ordered sequence of XML nodes. This sequence is what + xmltable transforms into output rows. + + + + + The SQL/XML standard specifies the the + xmltable construct should take an XQuery + expression as the first argument, but PostgreSQL currently only + supports XPath, which is a subset of XQuery. + + + + + The PASSING xml clause provides the + XML document to operate on. + The BY REF clauses have no effect in + PostgreSQL, but are allowed for SQL conformance and compatibility + with other implementations. Per the SQL/XML standard, the first + BY REF is required, the second is optional. + Passing BY VALUE is not supported. Multiple + comma-separated terms in the PASSING clause are not supported. + AS aliases are not supported. The argument must be + a well-formed XML document; fragments/forests are not accepted. + + + + The optional COLUMNS clause specifies the list + of colums in the generated table. If the whole + COLUMNS list is omitted, then the result set is a + single column where each row is a field of xml type + containing the data matched by the rowexpr. + Otherwise each entry describes a single column. See the syntax + summary above for the format. The column name and type are + required; the path and not-null specifier are optional. + 10'); + + xmltable +------------------------------ + 10 +]]> + + + + + Column names and types are identifiers so unlike normal + function arguments they may not be specified as bind parameters, + column references, or expressions. The path and default are + ordinary values. + + + + + The PATH for a column is an xpath expression that is + evaluated for each row, relative to the result of the + rowexpr, to find the value of the column. + If no PATH is given then the column name is used as an + implicit path. + + + + The text body of the XML matched by the PATH expression is + used as the column value. Multiple text() nodes within + an element are concatenated in order. Any child elements, processing + instructions, and comments are ignored, but the text contents of child + elements are concatenated to the result: + a1aa2a bbbbxxxcccc' COLUMNS element text); +element +----------------- + a1aa2a bbbbcccc + (1 row) +]]> + Note that the whitespace-only text() node between two non-text + elements is preserved, and that leading whitespace on a text() + node is not flattened. + + + + A PATH expression that matches multiple top-level elements + results in an error: + 12' COLUMNS element text); +ERROR: more than one value returned by column XPath expression +]]> + This applies to text() nodes too, so avoid using explicit text nodes + in your path expressions unless this behaviour is desired: + a1aa2a' COLUMNS element text PATH 'element/text()'); +ERROR: more than one value returned by column XPath expression +]]> + If the PATH matches an empty tag the result is an empty string + (not NULL). Any xsi:nil attributes are ignored. + + + + The five predefined XML entities 'apos;, + 'quot;, 'amp;, 'lt; + and 'gt; are expanded to their corresponding characters + ', ", &, < + and > on processing. If the output column format is + xml then the characters <, > and + & are converted to XML entities, but " and + ' are left unchanged. This means that 'apos; + and 'quot; entities in XML input get expanded in xml + output columns. There is no way to prevent their expansion. + + + + If the path expression does not match for a given row but a + DEFAULT expression is specified, the resulting + default value is used. If no DEFAULT is given the + field will be NULL. Unlike normal queries, it is possible for + a DEFAULT expression to reference the value of output columns + that appear prior to it in the column-list, so the default of one + column may be based on the value of another column. + + + + Columns may be marked NOT NULL. If the xpath expression + for a NOT NULL column does not match anything and there + is no DEFAULT or the DEFAULT also evaluated + to null then the function terminates with an ERROR. + + + + + Unlike normal PostgreSQL functions, the PATH and + DEFAULT arguments to xmltable are not evaluated + to a simple value before calling the function. PATH + expressions are normally evaluated exactly once per input row + , and DEFAULT expressions each time a default is + needed for a field. If the expression qualifies as stable or immutable + the repeat evaluation may be skipped. Effectively xmltable + behaves more like a subquery than a function call. This means that you + can usefully use volatile functions like nextval in + DEFAULT expressions, your PATH expressions may + depend on other parts of the XML document, etc. + + + + + A column marked with the + FOR ORDINALITY keyword will be populated with + row numbers that match the order in which the the output rows appeared + in the original input XML document. Only one column should be + marked FOR ORDINALITY. + + + + Only XPath query language is supported. PostgreSQL doesn't support XQuery + language. Then the syntax of xmltable doesn't + allow to use XQuery related functionality - the name of xml expression + (clause AS) is not allowed, and only one xml expression + should be passed to xmltable function as parameter. + + diff --git a/src/backend/executor/execQual.c b/src/backend/executor/execQual.c index 743e7d6..321cb77 100644 --- a/src/backend/executor/execQual.c +++ b/src/backend/executor/execQual.c @@ -43,6 +43,7 @@ #include "catalog/pg_type.h" #include "executor/execdebug.h" #include "executor/nodeSubplan.h" +#include "executor/tableexpr.h" #include "funcapi.h" #include "miscadmin.h" #include "nodes/makefuncs.h" @@ -189,6 +190,9 @@ static Datum ExecEvalCurrentOfExpr(ExprState *exprstate, ExprContext *econtext, static Datum ExecEvalGroupingFuncExpr(GroupingFuncExprState *gstate, ExprContext *econtext, bool *isNull, ExprDoneCond *isDone); +static Datum ExecEvalTableExpr(TableExprState * tstate, + ExprContext *econtext, + bool *isnull, ExprDoneCond *isDone); /* ---------------------------------------------------------------- @@ -4500,6 +4504,234 @@ ExecEvalCurrentOfExpr(ExprState *exprstate, ExprContext *econtext, return 0; /* keep compiler quiet */ } +/* ---------------------------------------------------------------- + * ExecEvalTableExprProtected + * + * Evaluate a TableExpr node. This function may leak libxml memory, so it + * needs to be protected from exceptions. + * + * This function creates a TableExprBuilder object and applies all necessary + * settings; one tuple is returned per call. + * + * Is possible to go over this + * cycle more times per query - when LATERAL JOIN is used. On the end + * of cycle, the TableExprBuilder object is destroyed, state pointer + * to this object is cleaned, and related memory context is resetted. + * New call starts new cycle. + * ---------------------------------------------------------------- + */ +static Datum +ExecEvalTableExprProtected(TableExprState * tstate, + ExprContext *econtext, + bool *isNull, ExprDoneCond *isDone) +{ + TupleDesc tupdesc; + Datum result; + int i; + Datum value; + bool isnull; + const TableExprBuilder *builder; + void *builderCxt; + + tupdesc = tstate->tupdesc; + builder = tstate->builder; + builderCxt = tstate->builderCxt; + + /* + * First time around? Set up our calling context and evaluate the + * document expression. + */ + if (builderCxt == NULL) + { + ListCell *ns; + + builderCxt = builder->CreateContext(tupdesc, + tstate->in_functions, + tstate->typioparams, + tstate->per_rowset_memory); + tstate->builderCxt = builderCxt; + + /* Evaluate document expression first */ + value = ExecEvalExpr(tstate->expr, econtext, &isnull, NULL); + if (isnull) + { + *isDone = ExprSingleResult; + *isNull = true; + return (Datum) 0; + } + + /* + * The content can be bigger document and transformation to cstring + * can be expensive. The table builder is better place for this task - + * pass value as Datum. + */ + builder->SetContent(builderCxt, value); + + /* Evaluate namespace specifications */ + foreach(ns, tstate->namespaces) + { + Node *n = (Node *) lfirst(ns); + ExprState *expr; + char *ns_name; + char *ns_uri; + + if (IsA(n, NamedArgExpr)) + { + NamedArgExpr *na = (NamedArgExpr *) n; + + expr = (ExprState *) na->arg; + ns_name = na->name; + } + else + { + expr = (ExprState *) n; + ns_name = NULL; + } + + value = ExecEvalExpr((ExprState *) expr, econtext, &isnull, NULL); + if (isnull) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("namespace uri must not be null"))); + ns_uri = TextDatumGetCString(value); + + builder->AddNS(builderCxt, ns_name, ns_uri); + } + + /* Evaluate row path filter */ + value = ExecEvalExpr(tstate->row_path_expr, econtext, &isnull, NULL); + if (isnull) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("row query must not be null"))); + builder->SetRowPath(builderCxt, TextDatumGetCString(value)); + + /* Evaluate column paths */ + for (i = 0; i < tstate->ncols; i++) + { + char *col_path; + + if (tstate->col_path_expr[i] != NULL) + { + value = ExecEvalExpr(tstate->col_path_expr[i], econtext, &isnull, NULL); + if (isnull) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("column path for column \"%s\" must not be null", + NameStr(tupdesc->attrs[i]->attname)))); + col_path = TextDatumGetCString(value); + } + else + col_path = NameStr(tupdesc->attrs[i]->attname); + + builder->SetColumnPath(builderCxt, col_path, i); + } + } + + /* Now we can prepare result */ + if (builder->FetchRow(builderCxt)) + { + HeapTuple tuple; + HeapTupleHeader dtuple; + Datum *values; + bool *nulls; + + values = tstate->values; + nulls = tstate->nulls; + + for (i = 0; i < tupdesc->natts; i++) + { + if (i != tstate->for_ordinality_col - 1) + { + values[i] = builder->GetValue(builderCxt, i, &isnull); + + if (isnull && tstate->def_expr[i] != NULL) + values[i] = ExecEvalExpr(tstate->def_expr[i], econtext, &isnull, NULL); + + if (isnull && tstate->not_null[i]) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null is not allowed in column \"%s\"", + NameStr(tupdesc->attrs[i]->attname)))); + nulls[i] = isnull; + } + else + { + values[i] = Int32GetDatum(++tstate->rownum); + nulls[i] = false; + } + } + + tuple = heap_form_tuple(tupdesc, values, nulls); + dtuple = (HeapTupleHeader) palloc(tuple->t_len); + memcpy(dtuple, tuple->t_data, tuple->t_len); + + /* + * Label the datum with the composite type previously identified + */ + HeapTupleHeaderSetTypeId(dtuple, tupdesc->tdtypeid); + HeapTupleHeaderSetTypMod(dtuple, tupdesc->tdtypmod); + + heap_freetuple(tuple); + + *isNull = false; + *isDone = ExprMultipleResult; + + result = HeapTupleHeaderGetDatum(dtuple); + } + else + { + /* no more rows */ + builder->DestroyContext(builderCxt); + tstate->builderCxt = NULL; + + /* ensure releasing all memory */ + MemoryContextReset(tstate->per_rowset_memory); + + *isNull = true; + *isDone = ExprEndResult; + + result = (Datum) 0; + } + + return result; +} + +/* + * ExecEvalTableExpr - this is envelop of ExecEvalTableExprProtected() function. + * + * This function ensures releasing all TableBuilder context and related + * memory context, when ExecEvalTableExprProtected fails on exception. + */ +static Datum +ExecEvalTableExpr(TableExprState * tstate, + ExprContext *econtext, + bool *isNull, ExprDoneCond *isDone) +{ + /* Ensure releasing context every exception */ + Datum result; + + PG_TRY(); + { + result = ExecEvalTableExprProtected(tstate, econtext, isNull, isDone); + } + PG_CATCH(); + { + if (tstate->builderCxt != NULL) + { + tstate->builder->DestroyContext(tstate->builderCxt); + tstate->builderCxt = NULL; + } + + MemoryContextDelete(tstate->per_rowset_memory); + tstate->per_rowset_memory = NULL; + + PG_RE_THROW(); + } + PG_END_TRY(); + + return result; +} /* * ExecEvalExprSwitchContext @@ -5262,6 +5494,109 @@ ExecInitExpr(Expr *node, PlanState *parent) /* Don't fall through to the "common" code below */ return (ExprState *) outlist; } + case T_TableExpr: + { + TableExpr *te = (TableExpr *) node; + TableExprState *tstate = makeNode(TableExprState); + int ncols; + int i; + + tstate->xprstate.evalfunc = (ExprStateEvalFunc) ExecEvalTableExpr; + tstate->builderCxt = NULL; + + /* Only XmlTableBuilder is supported currently */ + tstate->builder = &XmlTableBuilder; + + /* XXX this assumes the anon record type has been registered */ + tstate->tupdesc = + lookup_rowtype_tupdesc_copy(exprType((Node *) te), + exprTypmod((Node *) te)); + Assert(tstate->tupdesc->natts > 0); + + ncols = tstate->tupdesc->natts; + tstate->values = palloc0(sizeof(Datum) * ncols); + tstate->nulls = palloc(sizeof(bool) * ncols); + tstate->in_functions = palloc(sizeof(FmgrInfo) * ncols); + tstate->typioparams = palloc(sizeof(Oid) * ncols); + + for (i = 0; i < ncols; i++) + { + Oid in_funcid; + + getTypeInputInfo(tstate->tupdesc->attrs[i]->atttypid, + &in_funcid, &tstate->typioparams[i]); + fmgr_info(in_funcid, &tstate->in_functions[i]); + } + + tstate->row_path_expr = ExecInitExpr((Expr *) te->row_path, parent); + tstate->expr = ExecInitExpr((Expr *) te->expr, parent); + + if (te->cols) + { + ListCell *col; + + Assert(ncols == list_length(te->cols)); + + tstate->def_expr = palloc0(sizeof(ExprState *) * ncols); + tstate->col_path_expr = palloc0(sizeof(ExprState *) * ncols); + tstate->not_null = palloc0(sizeof(bool) * ncols); + tstate->ncols = ncols; + + i = 0; + foreach(col, te->cols) + { + TableExprColumn *tec = (TableExprColumn *) lfirst(col); + + if (!tec->for_ordinality) + { + tstate->def_expr[i] = ExecInitExpr((Expr *) tec->default_expr, + parent); + tstate->col_path_expr[i] = ExecInitExpr((Expr *) tec->path_expr, + parent); + tstate->not_null[i] = tec->is_not_null; + } + else + tstate->for_ordinality_col = i + 1; + + i++; + } + } + + if (te->namespaces) + { + List *preparedlist = NIL; + ListCell *ns; + + foreach(ns, te->namespaces) + { + Node *n = (Node *) lfirst(ns); + + if (IsA(n, NamedArgExpr)) + { + NamedArgExpr *na = (NamedArgExpr *) n; + NamedArgExpr *nax = makeNode(NamedArgExpr); + + nax->name = na->name; + nax->arg = (Expr *) ExecInitExpr(na->arg, parent); + nax->location = na->location; + preparedlist = lappend(preparedlist, nax); + } + else + preparedlist = lappend(preparedlist, + ExecInitExpr((Expr *) n, parent)); + } + tstate->namespaces = preparedlist; + } + else + tstate->namespaces = NIL; + + tstate->per_rowset_memory = AllocSetContextCreate(CurrentMemoryContext, + "XmlTable per rowgroup context", + ALLOCSET_DEFAULT_SIZES); + + state = (ExprState *) tstate; + } + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); diff --git a/src/backend/executor/execTuples.c b/src/backend/executor/execTuples.c index 533050d..3f0b490 100644 --- a/src/backend/executor/execTuples.c +++ b/src/backend/executor/execTuples.c @@ -975,6 +975,48 @@ ExecTypeFromExprList(List *exprList) } /* + * Build a tuple descriptor for TableExpr using its declared column list, and + * assign it a record's typmod. + */ +TupleDesc +ExecTypeFromTableExpr(const TableExpr *te) +{ + TupleDesc typeInfo; + + if (te->cols != NIL) + { + ListCell *col; + int i = 1; + + typeInfo = CreateTemplateTupleDesc(list_length(te->cols), false); + + foreach(col, te->cols) + { + TableExprColumn *tec = (TableExprColumn *) lfirst(col); + + TupleDescInitEntry(typeInfo, + (AttrNumber) i, + pstrdup(tec->colname), + tec->typid, + tec->typmod, + 0); + i++; + } + } + else + { + typeInfo = CreateTemplateTupleDesc(1, false); + TupleDescInitEntry(typeInfo, (AttrNumber) 1, + "xmltable", XMLOID, -1, 0); + } + + /* XXX OK to do this? looks a bit out of place ... */ + assign_record_type_typmod(typeInfo); + + return typeInfo; +} + +/* * ExecTypeSetColNames - set column names in a TupleDesc * * Column names must be provided as an alias list (list of String nodes). diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 04e49b7..b9c39d7 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -1987,6 +1987,63 @@ _copyOnConflictExpr(const OnConflictExpr *from) return newnode; } +/* + * _copyTableExpr + */ +static TableExpr * +_copyTableExpr(const TableExpr * from) +{ + TableExpr *newnode = makeNode(TableExpr); + + COPY_NODE_FIELD(row_path); + COPY_NODE_FIELD(expr); + COPY_NODE_FIELD(cols); + COPY_NODE_FIELD(namespaces); + COPY_LOCATION_FIELD(location); + + return newnode; +} + +/* + * _copyTableExprRawCol + */ +static TableExprRawCol * +_copyTableExprRawCol(const TableExprRawCol * from) +{ + TableExprRawCol *newnode = makeNode(TableExprRawCol); + + COPY_STRING_FIELD(colname); + COPY_NODE_FIELD(typeName); + COPY_SCALAR_FIELD(for_ordinality); + COPY_SCALAR_FIELD(is_not_null); + COPY_NODE_FIELD(path_expr); + COPY_NODE_FIELD(default_expr); + COPY_LOCATION_FIELD(location); + + return newnode; +} + +/* + * _copyTableExprColumn + */ +static TableExprColumn * +_copyTableExprColumn(const TableExprColumn * from) +{ + TableExprColumn *newnode = makeNode(TableExprColumn); + + COPY_STRING_FIELD(colname); + COPY_SCALAR_FIELD(typid); + COPY_SCALAR_FIELD(typmod); + COPY_SCALAR_FIELD(collation); + COPY_SCALAR_FIELD(for_ordinality); + COPY_SCALAR_FIELD(is_not_null); + COPY_NODE_FIELD(path_expr); + COPY_NODE_FIELD(default_expr); + COPY_LOCATION_FIELD(location); + + return newnode; +} + /* **************************************************************** * relation.h copy functions * @@ -4600,6 +4657,12 @@ copyObject(const void *from) case T_OnConflictExpr: retval = _copyOnConflictExpr(from); break; + case T_TableExpr: + retval = _copyTableExpr(from); + break; + case T_TableExprColumn: + retval = _copyTableExprColumn(from); + break; /* * RELATION NODES @@ -5104,6 +5167,9 @@ copyObject(const void *from) case T_TriggerTransition: retval = _copyTriggerTransition(from); break; + case T_TableExprRawCol: + retval = _copyTableExprRawCol(from); + break; /* * MISCELLANEOUS NODES diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 2eaf41c..686bccb 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -2615,6 +2615,36 @@ _equalCommonTableExpr(const CommonTableExpr *a, const CommonTableExpr *b) } static bool +_equalTableExprRawCol(const TableExprRawCol * a, const TableExprRawCol * b) +{ + COMPARE_STRING_FIELD(colname); + COMPARE_NODE_FIELD(typeName); + COMPARE_SCALAR_FIELD(for_ordinality); + COMPARE_SCALAR_FIELD(is_not_null); + COMPARE_NODE_FIELD(path_expr); + COMPARE_NODE_FIELD(default_expr); + COMPARE_LOCATION_FIELD(location); + + return true; +} + +static bool +_equalTableExprColumn(const TableExprColumn * a, const TableExprColumn * b) +{ + COMPARE_STRING_FIELD(colname); + COMPARE_SCALAR_FIELD(typid); + COMPARE_SCALAR_FIELD(typmod); + COMPARE_SCALAR_FIELD(collation); + COMPARE_SCALAR_FIELD(for_ordinality); + COMPARE_SCALAR_FIELD(is_not_null); + COMPARE_NODE_FIELD(path_expr); + COMPARE_NODE_FIELD(default_expr); + COMPARE_LOCATION_FIELD(location); + + return true; +} + +static bool _equalXmlSerialize(const XmlSerialize *a, const XmlSerialize *b) { COMPARE_SCALAR_FIELD(xmloption); @@ -2645,6 +2675,18 @@ _equalTriggerTransition(const TriggerTransition *a, const TriggerTransition *b) return true; } +static bool +_equalTableExpr(const TableExpr * a, const TableExpr * b) +{ + COMPARE_NODE_FIELD(row_path); + COMPARE_NODE_FIELD(expr); + COMPARE_NODE_FIELD(cols); + COMPARE_NODE_FIELD(namespaces); + COMPARE_LOCATION_FIELD(location); + + return true; +} + /* * Stuff from pg_list.h */ @@ -2910,6 +2952,12 @@ equal(const void *a, const void *b) case T_JoinExpr: retval = _equalJoinExpr(a, b); break; + case T_TableExpr: + retval = _equalTableExpr(a, b); + break; + case T_TableExprColumn: + retval = _equalTableExprColumn(a, b); + break; /* * RELATION NODES @@ -3401,6 +3449,9 @@ equal(const void *a, const void *b) case T_TriggerTransition: retval = _equalTriggerTransition(a, b); break; + case T_TableExprRawCol: + retval = _equalTableExprRawCol(a, b); + break; default: elog(ERROR, "unrecognized node type: %d", diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 3997441..df357e1 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -23,6 +23,7 @@ #include "nodes/relation.h" #include "utils/builtins.h" #include "utils/lsyscache.h" +#include "utils/typcache.h" static bool expression_returns_set_walker(Node *node, void *context); @@ -257,6 +258,12 @@ exprType(const Node *expr) case T_PlaceHolderVar: type = exprType((Node *) ((const PlaceHolderVar *) expr)->phexpr); break; + case T_TableExpr: + type = RECORDOID; + break; + case T_TableExprColumn: + type = ((const TableExprColumn *) expr)->typid; + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); type = InvalidOid; /* keep compiler quiet */ @@ -492,6 +499,19 @@ exprTypmod(const Node *expr) return ((const SetToDefault *) expr)->typeMod; case T_PlaceHolderVar: return exprTypmod((Node *) ((const PlaceHolderVar *) expr)->phexpr); + case T_TableExpr: + { + /* + * The result of table expression is pseudo-type record. + * Generate a blessed tupdesc from the column definitions, and + * returns its typmod. + */ + TupleDesc tupdesc = ExecTypeFromTableExpr((const TableExpr *) expr); + + return tupdesc->tdtypmod; + } + case T_TableExprColumn: + return ((const TableExprColumn *) expr)->typmod; default: break; } @@ -727,6 +747,8 @@ expression_returns_set_walker(Node *node, void *context) return false; if (IsA(node, XmlExpr)) return false; + if (IsA(node, TableExpr)) + return true; return expression_tree_walker(node, expression_returns_set_walker, context); @@ -929,6 +951,12 @@ exprCollation(const Node *expr) case T_PlaceHolderVar: coll = exprCollation((Node *) ((const PlaceHolderVar *) expr)->phexpr); break; + case T_TableExpr: + coll = InvalidOid; /* result is composite or XML or JSON */ + break; + case T_TableExprColumn: + coll = ((const TableExprColumn *) expr)->collation; + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); coll = InvalidOid; /* keep compiler quiet */ @@ -1127,6 +1155,13 @@ exprSetCollation(Node *expr, Oid collation) case T_CurrentOfExpr: Assert(!OidIsValid(collation)); /* result is always boolean */ break; + case T_TableExpr: + Assert(!OidIsValid(collation)); /* result is always composite + * or XML, .. */ + break; + case T_TableExprColumn: + ((TableExprColumn *) expr)->collation = collation; + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); break; @@ -1552,6 +1587,9 @@ exprLocation(const Node *expr) /* just use nested expr's location */ loc = exprLocation((Node *) ((const InferenceElem *) expr)->expr); break; + case T_TableExpr: + loc = ((const TableExpr *) expr)->location; + break; default: /* for any other node type it's just unknown... */ loc = -1; @@ -2211,6 +2249,30 @@ expression_tree_walker(Node *node, return true; } break; + case T_TableExpr: + { + TableExpr *te = (TableExpr *) node; + + if (walker(te->row_path, context)) + return true; + if (walker(te->expr, context)) + return true; + if (walker(te->namespaces, context)) + return true; + if (walker(te->cols, context)) + return true; + } + break; + case T_TableExprColumn: + { + TableExprColumn *tec = (TableExprColumn *) node; + + if (walker(tec->path_expr, context)) + return true; + if (walker(tec->default_expr, context)) + return true; + } + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); @@ -3007,6 +3069,30 @@ expression_tree_mutator(Node *node, return (Node *) newnode; } break; + case T_TableExpr: + { + TableExpr *te = (TableExpr *) node; + TableExpr *newnode; + + FLATCOPY(newnode, te, TableExpr); + MUTATE(newnode->row_path, te->row_path, Node *); + MUTATE(newnode->expr, te->expr, Node *); + MUTATE(newnode->namespaces, te->namespaces, List *); + MUTATE(newnode->cols, te->cols, List *); + return (Node *) newnode; + } + break; + case T_TableExprColumn: + { + TableExprColumn *tec = (TableExprColumn *) node; + TableExprColumn *newnode; + + FLATCOPY(newnode, tec, TableExprColumn); + MUTATE(newnode->path_expr, tec->path_expr, Node *); + MUTATE(newnode->default_expr, tec->default_expr, Node *); + return (Node *) newnode; + } + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); @@ -3622,6 +3708,20 @@ raw_expression_tree_walker(Node *node, break; case T_CommonTableExpr: return walker(((CommonTableExpr *) node)->ctequery, context); + case T_TableExpr: + { + TableExpr *te = (TableExpr *) node; + + if (walker(te->row_path, context)) + return true; + if (walker(te->expr, context)) + return true; + if (walker(te->cols, context)) + return true; + if (walker(te->namespaces, context)) + return true; + } + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 748b687..bb1b904 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -1574,6 +1574,48 @@ _outOnConflictExpr(StringInfo str, const OnConflictExpr *node) WRITE_NODE_FIELD(exclRelTlist); } +static void +_outTableExpr(StringInfo str, const TableExpr * node) +{ + WRITE_NODE_TYPE("TABLEEXPR"); + + WRITE_NODE_FIELD(row_path); + WRITE_NODE_FIELD(expr); + WRITE_NODE_FIELD(cols); + WRITE_NODE_FIELD(namespaces); + WRITE_LOCATION_FIELD(location); +} + +static void +_outTableExprColumn(StringInfo str, const TableExprColumn * node) +{ + WRITE_NODE_TYPE("TABLEEXPRCOLUMN"); + + WRITE_STRING_FIELD(colname); + WRITE_OID_FIELD(typid); + WRITE_INT_FIELD(typmod); + WRITE_OID_FIELD(collation); + WRITE_BOOL_FIELD(for_ordinality); + WRITE_BOOL_FIELD(is_not_null); + WRITE_NODE_FIELD(path_expr); + WRITE_NODE_FIELD(default_expr); + WRITE_LOCATION_FIELD(location); +} + +static void +_outTableExprRawCol(StringInfo str, const TableExprRawCol * node) +{ + WRITE_NODE_TYPE("TABLEEXPRCOLUMN"); + + WRITE_STRING_FIELD(colname); + WRITE_NODE_FIELD(typeName); + WRITE_BOOL_FIELD(for_ordinality); + WRITE_BOOL_FIELD(is_not_null); + WRITE_NODE_FIELD(path_expr); + WRITE_NODE_FIELD(default_expr); + WRITE_LOCATION_FIELD(location); +} + /***************************************************************************** * * Stuff from relation.h. @@ -3539,6 +3581,12 @@ outNode(StringInfo str, const void *obj) case T_XmlExpr: _outXmlExpr(str, obj); break; + case T_TableExpr: + _outTableExpr(str, obj); + break; + case T_TableExprColumn: + _outTableExprColumn(str, obj); + break; case T_NullTest: _outNullTest(str, obj); break; @@ -3865,6 +3913,9 @@ outNode(StringInfo str, const void *obj) case T_TriggerTransition: _outTriggerTransition(str, obj); break; + case T_TableExprRawCol: + _outTableExprRawCol(str, obj); + break; default: diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index 917e6c8..8a47a82 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -2266,6 +2266,44 @@ _readExtensibleNode(void) } /* + * _readTableExprNode + */ +static TableExpr * +_readTableExprNode(void) +{ + READ_LOCALS(TableExpr); + + READ_NODE_FIELD(row_path); + READ_NODE_FIELD(expr); + READ_NODE_FIELD(cols); + READ_NODE_FIELD(namespaces); + READ_LOCATION_FIELD(location); + + READ_DONE(); +} + +/* + * _readTableExprColumn + */ +static TableExprColumn * +_readTableExprColumnNode(void) +{ + READ_LOCALS(TableExprColumn); + + READ_STRING_FIELD(colname); + READ_OID_FIELD(typid); + READ_INT_FIELD(typmod); + READ_OID_FIELD(collation); + READ_BOOL_FIELD(for_ordinality); + READ_BOOL_FIELD(is_not_null); + READ_NODE_FIELD(path_expr); + READ_NODE_FIELD(default_expr); + READ_LOCATION_FIELD(location); + + READ_DONE(); +} + +/* * parseNodeString * * Given a character string representing a node tree, parseNodeString creates @@ -2497,6 +2535,10 @@ parseNodeString(void) return_value = _readAlternativeSubPlan(); else if (MATCH("EXTENSIBLENODE", 14)) return_value = _readExtensibleNode(); + else if (MATCH("TABLEEXPR", 9)) + return_value = _readTableExprNode(); + else if (MATCH("TABLEEXPRCOLUMN", 15)) + return_value = _readTableExprColumnNode(); else { elog(ERROR, "badly formatted node string \"%.32s\"...", token); diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 9598f28..8bfd06d 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -829,6 +829,12 @@ expression_returns_set_rows_walker(Node *node, double *count) *count *= get_func_rows(expr->opfuncid); } } + if (IsA(node, TableExpr)) + { + /* we have not any method how to estimate it, use default */ + *count = 1000; + return false; + } /* Avoid recursion for some cases that can't return a set */ if (IsA(node, Aggref)) @@ -3581,6 +3587,33 @@ eval_const_expressions_mutator(Node *node, context); } break; + case T_TableExprColumn: + { + TableExprColumn *tec = (TableExprColumn *) node; + + if (tec->path_expr != NULL || tec->default_expr != NULL) + { + TableExprColumn *newtec = makeNode(TableExprColumn); + + newtec->colname = tec->colname; + newtec->typid = tec->typid; + newtec->typmod = tec->typmod; + newtec->collation = tec->collation; + newtec->for_ordinality = tec->for_ordinality; + newtec->is_not_null = tec->is_not_null; + newtec->path_expr = + eval_const_expressions_mutator((Node *) tec->path_expr, + context); + newtec->default_expr = + eval_const_expressions_mutator((Node *) tec->default_expr, + context); + newtec->location = tec->location; + + return (Node *) newtec; + } + } + break; + default: break; } diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 0ec1cd3..2b71fdd 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -546,6 +546,13 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type opt_existing_window_name %type opt_if_not_exists +%type TableExprColList TableExprColOptions +%type TableExprCol +%type TableExprColOption +%type IsNotNull +%type XmlNamespaceList +%type XmlNamespace + /* * 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 @@ -577,10 +584,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CACHE CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE - CLUSTER COALESCE COLLATE COLLATION COLUMN COMMENT COMMENTS COMMIT - COMMITTED CONCURRENTLY CONFIGURATION CONFLICT CONNECTION CONSTRAINT - CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY COST CREATE - CROSS CSV CUBE CURRENT_P + CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS + COMMIT COMMITTED CONCURRENTLY 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 CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER CURSOR CYCLE @@ -650,8 +657,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE - XML_P XMLATTRIBUTES XMLCONCAT XMLELEMENT XMLEXISTS XMLFOREST XMLPARSE - XMLPI XMLROOT XMLSERIALIZE + XML_P XMLATTRIBUTES XMLCONCAT XMLELEMENT XMLEXISTS XMLFOREST XMLNAMESPACES + XMLPARSE XMLPI XMLROOT XMLSERIALIZE XMLTABLE YEAR_P YES_P @@ -12611,6 +12618,165 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *)n; } + | XMLTABLE '(' c_expr xmlexists_argument ')' + { + TableExpr *n = makeNode(TableExpr); + n->row_path = $3; + n->expr = $4; + n->cols = NIL; + n->namespaces = NIL; + n->location = @1; + $$ = (Node *)n; + } + | XMLTABLE '(' c_expr xmlexists_argument COLUMNS TableExprColList ')' + { + TableExpr *n = makeNode(TableExpr); + n->row_path = $3; + n->expr = $4; + n->cols = $6; + n->namespaces = NIL; + n->location = @1; + $$ = (Node *)n; + } + | XMLTABLE '(' XMLNAMESPACES '(' XmlNamespaceList ')' ',' + c_expr xmlexists_argument ')' + { + TableExpr *n = makeNode(TableExpr); + n->row_path = $8; + n->expr = $9; + n->cols = NIL; + n->namespaces = $5; + n->location = @1; + $$ = (Node *)n; + } + | XMLTABLE '(' XMLNAMESPACES '(' XmlNamespaceList ')' ',' + c_expr xmlexists_argument COLUMNS TableExprColList ')' + { + TableExpr *n = makeNode(TableExpr); + n->row_path = $8; + n->expr = $9; + n->cols = $11; + n->namespaces = $5; + n->location = @1; + $$ = (Node *)n; + } + ; + +TableExprColList: TableExprCol { $$ = list_make1($1); } + | TableExprColList ',' TableExprCol { $$ = lappend($1, $3); } + ; + +TableExprCol: + ColId Typename IsNotNull + { + TableExprRawCol *rawc = makeNode(TableExprRawCol); + + rawc->colname = $1; + rawc->for_ordinality = false; + rawc->typeName = $2; + rawc->is_not_null = $3; + rawc->path_expr = NULL; + rawc->default_expr = NULL; + rawc->location = @1; + + $$ = (Node *) rawc; + } + | ColId Typename TableExprColOptions IsNotNull + { + TableExprRawCol *rawc = makeNode(TableExprRawCol); + ListCell *option; + + rawc->colname = $1; + rawc->for_ordinality = false; + rawc->typeName = $2; + rawc->is_not_null = $4; + rawc->path_expr = NULL; + rawc->default_expr = NULL; + rawc->location = @1; + + foreach(option, $3) + { + DefElem *defel = (DefElem *) lfirst(option); + + if (strcmp(defel->defname, "default") == 0) + { + if (rawc->default_expr != NULL) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("only one DEFAULT value per column is allowed"), + parser_errposition(defel->location))); + rawc->default_expr = defel->arg; + } + else if (strcmp(defel->defname, "path") == 0) + { + if (rawc->path_expr != NULL) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("only one PATH value per column is allowed"), + parser_errposition(defel->location))); + rawc->path_expr = defel->arg; + } + else + { + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("unrecognized column option \"%s\"", + defel->defname), + parser_errposition(defel->location))); + } + } + $$ = (Node *) rawc; + } + | ColId FOR ORDINALITY + { + TableExprRawCol *rawc = makeNode(TableExprRawCol); + + rawc->colname = $1; + rawc->for_ordinality = true; + /* other fields are ignored, initialized by makeNode */ + rawc->location = @1; + + $$ = (Node *) rawc; + } + ; + +TableExprColOptions: TableExprColOption { $$ = list_make1($1); } + | TableExprColOptions TableExprColOption { $$ = lappend($1, $2); } + ; + +TableExprColOption: + IDENT a_expr + { + $$ = makeDefElem($1, $2, @1); + } + | DEFAULT a_expr + { + $$ = makeDefElem("default", $2, @1); + } + ; + +IsNotNull: NOT NULL_P { $$ = true; } + | NULL_P { $$ = false; } + | /* EMPTY */ { $$ = false; } + ; + +XmlNamespaceList: XmlNamespace { $$ = list_make1($1); } + | XmlNamespaceList ',' XmlNamespace { $$ = lappend($1, $3); } + ; + +XmlNamespace: + a_expr AS ColLabel + { + NamedArgExpr *na = makeNode(NamedArgExpr); + na->name = $3; + na->arg = (Expr *) $1; + na->location = @1; + $$ = (Node *) na; + } + | DEFAULT a_expr + { + $$ = $2; + } ; /* @@ -13749,6 +13915,7 @@ unreserved_keyword: | CLASS | CLOSE | CLUSTER + | COLUMNS | COMMENT | COMMENTS | COMMIT @@ -14050,10 +14217,12 @@ col_name_keyword: | XMLELEMENT | XMLEXISTS | XMLFOREST + | XMLNAMESPACES | XMLPARSE | XMLPI | XMLROOT | XMLSERIALIZE + | XMLTABLE ; /* Type/function identifier --- keywords that can be type or function names. diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c index d277fd6..12f3b36 100644 --- a/src/backend/parser/parse_coerce.c +++ b/src/backend/parser/parse_coerce.c @@ -1095,9 +1095,9 @@ coerce_to_boolean(ParseState *pstate, Node *node, } /* - * coerce_to_specific_type() - * Coerce an argument of a construct that requires a specific data type. - * Also check that input is not a set. + * coerce_to_specific_type_typmod() + * Coerce an argument of a construct that requires a specific data type, + * with a specific typmod. Also check that input is not a set. * * Returns the possibly-transformed node tree. * @@ -1105,9 +1105,9 @@ coerce_to_boolean(ParseState *pstate, Node *node, * processing is wanted. */ Node * -coerce_to_specific_type(ParseState *pstate, Node *node, - Oid targetTypeId, - const char *constructName) +coerce_to_specific_type_typmod(ParseState *pstate, Node *node, + Oid targetTypeId, int32 targetTypmod, + const char *constructName) { Oid inputTypeId = exprType(node); @@ -1116,7 +1116,7 @@ coerce_to_specific_type(ParseState *pstate, Node *node, Node *newnode; newnode = coerce_to_target_type(pstate, node, inputTypeId, - targetTypeId, -1, + targetTypeId, targetTypmod, COERCION_ASSIGNMENT, COERCE_IMPLICIT_CAST, -1); @@ -1143,6 +1143,25 @@ coerce_to_specific_type(ParseState *pstate, Node *node, return node; } +/* + * coerce_to_specific_type() + * Coerce an argument of a construct that requires a specific data type. + * Also check that input is not a set. + * + * Returns the possibly-transformed node tree. + * + * As with coerce_type, pstate may be NULL if no special unknown-Param + * processing is wanted. + */ +Node * +coerce_to_specific_type(ParseState *pstate, Node *node, + Oid targetTypeId, + const char *constructName) +{ + return coerce_to_specific_type_typmod(pstate, node, + targetTypeId, -1, + constructName); +} /* * parser_coercion_errposition - report coercion error location, if possible diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 63f7965..d6a0559 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -37,6 +37,7 @@ #include "utils/date.h" #include "utils/lsyscache.h" #include "utils/timestamp.h" +#include "utils/typcache.h" #include "utils/xml.h" @@ -122,6 +123,7 @@ static Node *transformIndirection(ParseState *pstate, Node *basenode, List *indirection); static Node *transformTypeCast(ParseState *pstate, TypeCast *tc); static Node *transformCollateClause(ParseState *pstate, CollateClause *c); +static Node *transformTableExpr(ParseState *pstate, TableExpr * te); 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, @@ -365,6 +367,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) break; } + case T_TableExpr: + result = transformTableExpr(pstate, (TableExpr *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -2677,6 +2683,182 @@ transformCollateClause(ParseState *pstate, CollateClause *c) return (Node *) newc; } +static Node * +transformTableExpr(ParseState *pstate, TableExpr* te) +{ + TableExpr *newte = makeNode(TableExpr); + const char *constructName; + Oid exprType; + + /* Currently only XMLTABLE is supported */ + constructName = "XMLTABLE"; + exprType = XMLOID; + + /* TableExpr is SRF, check target SRF usage */ + check_srf_call_placement(pstate, te->location); + + /* Transform the row-generating Xpath expression ... */ + Assert(te->row_path != NULL); + newte->row_path = coerce_to_specific_type(pstate, + transformExprRecurse(pstate, te->row_path), + TEXTOID, + constructName); + /* ... and the XML itself */ + Assert(te->expr != NULL); + newte->expr = coerce_to_specific_type(pstate, + transformExprRecurse(pstate, te->expr), + exprType, + constructName); + + /* Columns, if any, also need to be transformed */ + if (te->cols != NIL) + { + ListCell *col; + bool for_ordinality = false; + char **colnames; + int i = 0; + + colnames = palloc(sizeof(char *) * list_length(te->cols)); + + /* + * For each TableExprRawCol in the input TableExpr, generate one + * TableExprColumn in the output TableExpr. + */ + foreach(col, te->cols) + { + TableExprRawCol *rawc = (TableExprRawCol *) lfirst(col); + TableExprColumn *newc; + Oid typid; + int32 typmod; + int j; + + /* Only one FOR ORDINALITY column is allowed; verify */ + if (rawc->for_ordinality) + { + if (for_ordinality) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("only one FOR ORDINALITY column is allowed"), + parser_errposition(pstate, rawc->location))); + for_ordinality = true; + } + + newc = makeNode(TableExprColumn); + newc->colname = pstrdup(rawc->colname); + newc->for_ordinality = rawc->for_ordinality; + newc->is_not_null = rawc->is_not_null; + newc->location = rawc->location; + + /* + * Determine the type and typmod for the new column. + * FOR ORDINALITY columns are INTEGER per spec; the others are + * user-specified. + */ + if (rawc->for_ordinality) + { + newc->typid = INT4OID; + newc->typmod = -1; + } + else + typenameTypeIdAndMod(pstate, rawc->typeName, + &newc->typid, &newc->typmod); + + /* Transform the PATH and DEFAULT expressions */ + if (rawc->path_expr) + newc->path_expr = coerce_to_specific_type(pstate, + transformExprRecurse(pstate, rawc->path_expr), + TEXTOID, + constructName); + if (rawc->default_expr) + newc->default_expr = coerce_to_specific_type_typmod(pstate, + transformExprRecurse(pstate, rawc->default_expr), + newc->typid, newc->typmod, + constructName); + + + newte->cols = lappend(newte->cols, newc); + + /* make sure column names are unique */ + for (j = 0; j < i; j++) + if (strcmp(colnames[j], rawc->colname) == 0) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("the column name \"%s\" is not unique", + rawc->colname), + parser_errposition(pstate, rawc->location))); + colnames[i] = rawc->colname; + + i++; + } + + pfree(colnames); + } + else + newte->cols = NIL; + + /* Namespaces, if any, also need to be transformed */ + if (te->namespaces != NIL) + { + List *transformlist = NIL; + ListCell *ns; + bool found_default_namespace = false; + int nnames = 0; + char **nsnames; + + nsnames = palloc(sizeof(char *) * list_length(te->namespaces)); + + foreach(ns, te->namespaces) + { + Node *n = (Node *) lfirst(ns); + + if (IsA(n, NamedArgExpr)) + { + NamedArgExpr *na = (NamedArgExpr *) n; + int i; + + /* make sure namespace names are unique */ + for (i = 0; i < nnames; i++) + if (strcmp(nsnames[i], na->name) == 0) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("the namespace \"%s\" is not unique", + na->name), + parser_errposition(pstate, na->location))); + nsnames[nnames++] = na->name; + + na->arg = (Expr *) coerce_to_specific_type(pstate, + transformExprRecurse(pstate, (Node *) na->arg), + TEXTOID, + constructName); + } + else + { + /* Only one DEFAULT namespace is allowed; verify */ + if (found_default_namespace) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("only one default namespace is allowed"), + parser_errposition(pstate, exprLocation(n)))); + found_default_namespace = true; + n = coerce_to_specific_type(pstate, + transformExprRecurse(pstate, n), + TEXTOID, + constructName); + } + + transformlist = lappend(transformlist, n); + } + newte->namespaces = transformlist; + pfree(nsnames); + } + else + newte->namespaces = NIL; + + newte->location = te->location; + + return (Node *) newte; +} + /* * Transform a "row compare-op row" construct * diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index a76c33f..edafb9a 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1848,6 +1848,13 @@ FigureColnameInternal(Node *node, char **name) case T_XmlSerialize: *name = "xmlserialize"; return 2; + case T_TableExpr: + /* + * Make TableExpr act like a regular function. Only + * XMLTABLE expr is supported in this moment. + */ + *name = "xmltable"; + return 2; default: break; } diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile index 0f51275..5a3715c 100644 --- a/src/backend/utils/adt/Makefile +++ b/src/backend/utils/adt/Makefile @@ -29,7 +29,7 @@ OBJS = acl.o amutils.o arrayfuncs.o array_expanded.o array_selfuncs.o \ tsquery_op.o tsquery_rewrite.o tsquery_util.o tsrank.o \ tsvector.o tsvector_op.o tsvector_parser.o \ txid.o uuid.o varbit.o varchar.o varlena.o version.o \ - windowfuncs.o xid.o xml.o + windowfuncs.o xid.o xml.o xpath_parser.o like.o: like.c like_match.c diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index a3a4174..92d5dd9 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -50,6 +50,7 @@ #include "parser/parse_agg.h" #include "parser/parse_func.h" #include "parser/parse_oper.h" +#include "parser/parse_type.h" #include "parser/parser.h" #include "parser/parsetree.h" #include "rewrite/rewriteHandler.h" @@ -8303,6 +8304,105 @@ get_rule_expr(Node *node, deparse_context *context, } break; + case T_TableExpr: + { + TableExpr *te = (TableExpr *) node; + + /* + * Deparse TableExpr - the is only one TableExpr producer, + * the function XMLTABLE. + */ + + /* c_expr shoud be closed in brackets */ + appendStringInfoString(buf, "XMLTABLE("); + + if (te->namespaces != NIL) + { + ListCell *ns; + bool first = true; + + appendStringInfoString(buf, "XMLNAMESPACES("); + foreach(ns, te->namespaces) + { + Node *n = (Node *) lfirst(ns); + + if (!first) + appendStringInfoString(buf, ", "); + else + first = false; + + if (IsA(n, NamedArgExpr)) + { + NamedArgExpr *na = (NamedArgExpr *) n; + + get_rule_expr((Node *) na->arg, context, true); + appendStringInfo(buf, " AS %s", quote_identifier(na->name)); + } + else + { + appendStringInfoString(buf, "DEFAULT "); + get_rule_expr(n, context, true); + } + } + appendStringInfoChar(buf, ')'); + } + + appendStringInfoChar(buf, '('); + get_rule_expr((Node *) te->row_path, context, true); + appendStringInfoString(buf, ") PASSING ("); + get_rule_expr((Node *) te->expr, context, true); + appendStringInfoChar(buf, ')'); + + if (te->cols != NIL) + { + ListCell *col; + bool first = true; + + appendStringInfoString(buf, " COLUMNS "); + + foreach(col, te->cols) + { + TableExprColumn *tec = (TableExprColumn *) lfirst(col); + + if (!first) + appendStringInfoString(buf, ", "); + else + first = false; + + appendStringInfoString(buf, quote_identifier(tec->colname)); + appendStringInfoChar(buf, ' '); + + if (!tec->for_ordinality) + { + appendStringInfoString(buf, + format_type_with_typemod(tec->typid, tec->typmod)); + + if (tec->default_expr != NULL) + { + appendStringInfoString(buf, " DEFAULT ("); + get_rule_expr((Node *) tec->default_expr, context, true); + appendStringInfoChar(buf, ')'); + } + if (tec->path_expr != NULL) + { + appendStringInfoString(buf, " PATH ("); + get_rule_expr((Node *) tec->path_expr, context, true); + appendStringInfoChar(buf, ')'); + } + if (tec->is_not_null) + appendStringInfoString(buf, " NOT NULL"); + } + else + { + appendStringInfoString(buf, "FOR ORDINALITY"); + } + } + } + + appendStringInfoChar(buf, ')'); + } + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); break; diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c index 057c3bf..e979cf0 100644 --- a/src/backend/utils/adt/xml.c +++ b/src/backend/utils/adt/xml.c @@ -73,6 +73,7 @@ #include "commands/dbcommands.h" #include "executor/executor.h" #include "executor/spi.h" +#include "executor/tableexpr.h" #include "fmgr.h" #include "lib/stringinfo.h" #include "libpq/pqformat.h" @@ -89,6 +90,7 @@ #include "utils/rel.h" #include "utils/syscache.h" #include "utils/xml.h" +#include "utils/xpath_parser.h" /* GUC variables */ @@ -165,6 +167,29 @@ static void SPI_sql_row_to_xmlelement(uint64 rownum, StringInfo result, char *tablename, bool nulls, bool tableforest, const char *targetns, bool top_level); +static void *XmlTableCreateContext(TupleDesc tupdesc, + FmgrInfo *in_functions, Oid *typioparms, + MemoryContext mcxt); +static void XmlTableSetContent(void *tcontext, Datum value); +static void XmlTableAddNS(void *tcontext, char *name, char *uri); +static void XmlTableSetRowPath(void *tcontext, char *path); +static void XmlTableSetColumnPath(void *tcontext, char *path, int colnum); +static bool XmlTableFetchRow(void *tcontext); +static Datum XmlTableGetValue(void *tcontext, int colnum, bool *isnull); +static void XmlTableDestroyContext(void *tcontext); + +const TableExprBuilder XmlTableBuilder = +{ + XmlTableCreateContext, + XmlTableSetContent, + XmlTableAddNS, + XmlTableSetRowPath, + XmlTableSetColumnPath, + XmlTableFetchRow, + XmlTableGetValue, + XmlTableDestroyContext +}; + #define NO_XML_SUPPORT() \ ereport(ERROR, \ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), \ @@ -4065,3 +4090,588 @@ xml_is_well_formed_content(PG_FUNCTION_ARGS) return 0; #endif /* not USE_LIBXML */ } + +/* + * support functions for XMLTABLE function + * + */ +#ifdef USE_LIBXML + +/* + * Functions for XmlTableBuilder + * + */ +typedef struct XmlTableContext +{ + TupleDesc tupdesc; + MemoryContext per_rowset_memory; + char *def_namespace_name; + FmgrInfo *in_functions; + Oid *typioparams; + long int rc; + PgXmlErrorContext *xmlerrcxt; + xmlParserCtxtPtr ctxt; + xmlDocPtr doc; + xmlXPathContextPtr xpathcxt; + xmlXPathCompExprPtr xpathcomp; + xmlXPathObjectPtr xpathobj; + xmlXPathCompExprPtr *xpathscomp; +} XmlTableContext; + +/* + * Convert XML node to cstring (dump subtree in case of element, + * return value otherwise) + */ +static char * +xml_xmlnodetostr(xmlNodePtr cur, PgXmlErrorContext *xmlerrcxt) +{ + char *result; + + if (cur->type == XML_ELEMENT_NODE) + { + xmlBufferPtr buf; + xmlNodePtr cur_copy; + + buf = xmlBufferCreate(); + + /* + * The result of xmlNodeDump() won't contain namespace definitions + * from parent nodes, but xmlCopyNode() duplicates a node along with + * its required namespace definitions. + */ + cur_copy = xmlCopyNode(cur, 1); + + if (cur_copy == NULL) + xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, + "could not copy node"); + + PG_TRY(); + { + xmlNodeDump(buf, NULL, cur_copy, 0, 1); + result = pstrdup((const char *) xmlBufferContent(buf)); + } + PG_CATCH(); + { + xmlFreeNode(cur_copy); + xmlBufferFree(buf); + PG_RE_THROW(); + } + PG_END_TRY(); + xmlFreeNode(cur_copy); + xmlBufferFree(buf); + } + else + { + xmlChar *str; + + str = xmlXPathCastNodeToString(cur); + PG_TRY(); + { + /* Here we rely on XML having the same representation as TEXT */ + char *escaped = escape_xml((char *) str); + + result = escaped; + } + PG_CATCH(); + { + xmlFree(str); + PG_RE_THROW(); + } + PG_END_TRY(); + xmlFree(str); + } + + return result; +} + +/* + * Secure cstring for usage in libxml2 + */ +static xmlChar * +to_xmlstr(char *str, size_t len) +{ + xmlChar *result; + + result = (xmlChar *) palloc((len + 1) * sizeof(xmlChar)); + memcpy(result, str, len); + result[len] = '\0'; + + return result; +} +#endif + +/* + * Create XmlTableContext for XmlTable builder. Initialize XML parser. + */ +static void * +XmlTableCreateContext(TupleDesc tupdesc, + FmgrInfo *in_functions, Oid *typioparams, + MemoryContext per_rowset_memory) +{ +#ifdef USE_LIBXML + MemoryContext oldcxt; + XmlTableContext *result = NULL; + PgXmlErrorContext *xmlerrcxt = NULL; + volatile xmlParserCtxtPtr ctxt = NULL; + + oldcxt = MemoryContextSwitchTo(per_rowset_memory); + + result = palloc0(sizeof(struct XmlTableContext)); + result->tupdesc = tupdesc; + result->per_rowset_memory = per_rowset_memory; + result->in_functions = in_functions; + result->typioparams = typioparams; + result->xpathscomp = palloc0(sizeof(xmlXPathCompExprPtr) * tupdesc->natts); + + xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_ALL); + + PG_TRY(); + { + xmlInitParser(); + + ctxt = xmlNewParserCtxt(); + if (ctxt == NULL || xmlerrcxt->err_occurred) + xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, + "could not allocate parser context"); + } + PG_CATCH(); + { + if (ctxt != NULL) + xmlFreeParserCtxt(ctxt); + + pg_xml_done(xmlerrcxt, true); + + PG_RE_THROW(); + } + PG_END_TRY(); + + result->xmlerrcxt = xmlerrcxt; + result->ctxt = ctxt; + + MemoryContextSwitchTo(oldcxt); + + return result; +#else + NO_XML_SUPPORT(); +#endif /* not USE_LIBXML */ +} + +/* + * XmlTableSetContent + */ +static void +XmlTableSetContent(void *tcontext, Datum value) +{ +#ifdef USE_LIBXML + XmlTableContext *xtCxt = (XmlTableContext *) tcontext; + xmltype *xmlval = DatumGetXmlP(value); + xmlChar *xstr; + int length; + volatile xmlDocPtr doc = NULL; + volatile xmlXPathContextPtr xpathcxt = NULL; + MemoryContext oldcxt; + + length = VARSIZE(xmlval) - VARHDRSZ; + + oldcxt = MemoryContextSwitchTo(xtCxt->per_rowset_memory); + + xstr = to_xmlstr(VARDATA(xmlval), length); + + PG_TRY(); + { + doc = xmlCtxtReadMemory(xtCxt->ctxt, (char *) xstr, length, NULL, NULL, 0); + if (doc == NULL || xtCxt->xmlerrcxt->err_occurred) + xml_ereport(xtCxt->xmlerrcxt, ERROR, ERRCODE_INVALID_XML_DOCUMENT, + "could not parse XML document"); + xpathcxt = xmlXPathNewContext(doc); + if (xpathcxt == NULL || xtCxt->xmlerrcxt->err_occurred) + xml_ereport(xtCxt->xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, + "could not allocate XPath context"); + xpathcxt->node = xmlDocGetRootElement(doc); + if (xpathcxt->node == NULL || xtCxt->xmlerrcxt->err_occurred) + xml_ereport(xtCxt->xmlerrcxt, ERROR, ERRCODE_INTERNAL_ERROR, + "could not find root XML element"); + } + PG_CATCH(); + { + if (xpathcxt != NULL) + xmlXPathFreeContext(xpathcxt); + if (doc != NULL) + xmlFreeDoc(doc); + + PG_RE_THROW(); + } + PG_END_TRY(); + + xtCxt->doc = doc; + xtCxt->xpathcxt = xpathcxt; + + MemoryContextSwitchTo(oldcxt); +#else + NO_XML_SUPPORT(); +#endif /* not USE_LIBXML */ +} + +#define DEFAULT_NAMESPACE_NAME "pgdefnamespace" + +/* + * XmlTableAddNS - add namespace. use fake name when the name + * is null, and store this name as default namespace. + */ +static void +XmlTableAddNS(void *tcontext, char *name, char *uri) +{ +#ifdef USE_LIBXML + XmlTableContext *xtCxt = (XmlTableContext *) tcontext; + + if (name == NULL) + { + Assert(xtCxt->def_namespace_name == NULL); + + name = DEFAULT_NAMESPACE_NAME; + xtCxt->def_namespace_name = name; + } + + if (xmlXPathRegisterNs(xtCxt->xpathcxt, + to_xmlstr(name, strlen(name)), + to_xmlstr(uri, strlen(uri)))) + xml_ereport(xtCxt->xmlerrcxt, ERROR, ERRCODE_DATA_EXCEPTION, + "could not set XML namespace"); +#else + NO_XML_SUPPORT(); +#endif /* not USE_LIBXML */ +} + +/* + * XmlTableSetRowPath + */ +static void +XmlTableSetRowPath(void *tcontext, char *path) +{ +#ifdef USE_LIBXML + XmlTableContext *xtCxt = (XmlTableContext *) tcontext; + xmlChar *xstr; + MemoryContext oldcxt; + StringInfoData str; + + if (path == '\0') + ereport(ERROR, + (errcode(ERRCODE_DATA_EXCEPTION), + errmsg("row path filter must not be empty string"))); + + transformXPath(&str, path, NULL, xtCxt->def_namespace_name); + + oldcxt = MemoryContextSwitchTo(xtCxt->per_rowset_memory); + xstr = to_xmlstr(str.data, str.len); + + xtCxt->xpathcomp = xmlXPathCompile(xstr); + if (xtCxt->xpathcomp == NULL || xtCxt->xmlerrcxt->err_occurred) + xml_ereport(xtCxt->xmlerrcxt, ERROR, ERRCODE_DATA_EXCEPTION, + "invalid XPath expression"); + + MemoryContextSwitchTo(oldcxt); +#else + NO_XML_SUPPORT(); +#endif /* not USE_LIBXML */ +} + +/* + * XmlTableSetColumnPath + */ +static void +XmlTableSetColumnPath(void *tcontext, char *path, int colnum) +{ +#ifdef USE_LIBXML + XmlTableContext *xtCxt = (XmlTableContext *) tcontext; + MemoryContext oldcxt; + StringInfoData str; + xmlChar *xstr; + + if (path == '\0') + ereport(ERROR, + (errcode(ERRCODE_DATA_EXCEPTION), + errmsg("column path filter must not be empty string"))); + + transformXPath(&str, path, + "./", + xtCxt->def_namespace_name); + + oldcxt = MemoryContextSwitchTo(xtCxt->per_rowset_memory); + xstr = to_xmlstr(str.data, str.len); + + xtCxt->xpathscomp[colnum] = xmlXPathCompile(xstr); + if (xtCxt->xpathscomp[colnum] == NULL || xtCxt->xmlerrcxt->err_occurred) + xml_ereport(xtCxt->xmlerrcxt, ERROR, ERRCODE_DATA_EXCEPTION, + "invalid XPath expression"); + + MemoryContextSwitchTo(oldcxt); +#else + NO_XML_SUPPORT(); +#endif /* not USE_LIBXML */ +} + +/* + * XmlTableFetchRow - returns true when is possible to read + * values from XML document. + * + * First call evaluates row path over content. + * Next step and at other calls increment row counter and + * compare it with number values in result set. + */ +static bool +XmlTableFetchRow(void *tcontext) +{ +#ifdef USE_LIBXML + XmlTableContext *xtCxt = (XmlTableContext *) tcontext; + + /* + * XmlTable returns table - set of composite values. The error context, + * is used for producement more values, between two calls, there can be + * created and used another libxml2 error context. It is libxml2 global + * value, so it should be refreshed any time before any libxml2 usage, + * that is finished by returning some value. + */ + xmlSetStructuredErrorFunc((void *) xtCxt->xmlerrcxt, xml_errorHandler); + + if (xtCxt->xpathobj == NULL) + { + MemoryContext oldcxt = MemoryContextSwitchTo(xtCxt->per_rowset_memory); + + xtCxt->xpathobj = xmlXPathCompiledEval(xtCxt->xpathcomp, xtCxt->xpathcxt); + if (xtCxt->xpathobj == NULL || xtCxt->xmlerrcxt->err_occurred) + xml_ereport(xtCxt->xmlerrcxt, ERROR, ERRCODE_INTERNAL_ERROR, + "could not create XPath object"); + + xtCxt->rc = 0; + + MemoryContextSwitchTo(oldcxt); + } + + if (xtCxt->xpathobj->type == XPATH_NODESET) + { + if (xtCxt->xpathobj->nodesetval != NULL) + { + if (xtCxt->rc++ < xtCxt->xpathobj->nodesetval->nodeNr) + return true; + } + } + + return false; +#else + NO_XML_SUPPORT(); +#endif /* not USE_LIBXML */ + + return false; +} + +/* + * Apply column path on current node and returns result. + * Allows multivalue when expected type is XML. + * When COLUMNS part was not entered, returns current node. + */ +static Datum +XmlTableGetValue(void *tcontext, int colnum, bool *isnull) +{ +#ifdef USE_LIBXML + XmlTableContext *xtCxt = (XmlTableContext *) tcontext; + Datum result = (Datum) 0; + xmlNodePtr cur; + char *cstr = NULL; + + Assert(xtCxt->xpathobj && xtCxt->xpathobj->type == XPATH_NODESET && + xtCxt->xpathobj->nodesetval != NULL); + + /* Propagate context related error context to libxml2 */ + xmlSetStructuredErrorFunc((void *) xtCxt->xmlerrcxt, xml_errorHandler); + + cur = xtCxt->xpathobj->nodesetval->nodeTab[xtCxt->rc - 1]; + + if (xtCxt->xpathscomp[colnum] != NULL) + { + volatile xmlXPathObjectPtr column_xpathobj = NULL; + + /* fast leaving */ + if (cur->type != XML_ELEMENT_NODE) + elog(ERROR, "unexpected xmlNode type"); + + PG_TRY(); + { + /* Set current node as entry point for XPath evaluation */ + xmlXPathSetContextNode(cur, xtCxt->xpathcxt); + + /* Evaluate column path */ + column_xpathobj = xmlXPathCompiledEval(xtCxt->xpathscomp[colnum], xtCxt->xpathcxt); + if (column_xpathobj == NULL || xtCxt->xmlerrcxt->err_occurred) + xml_ereport(xtCxt->xmlerrcxt, ERROR, ERRCODE_INTERNAL_ERROR, + "could not create XPath object"); + + if (column_xpathobj->type == XPATH_NODESET) + { + int count; + + if (column_xpathobj->nodesetval != NULL) + count = column_xpathobj->nodesetval->nodeNr; + else + count = 0; + + if (count > 0) + { + Oid targettypid = xtCxt->tupdesc->attrs[colnum]->atttypid; + + if (count == 1) + { + if (targettypid != XMLOID) + { + xmlChar *str; + + str = xmlNodeListGetString(xtCxt->doc, + column_xpathobj->nodesetval->nodeTab[0]->xmlChildrenNode, + 1); + + if (str != NULL) + { + PG_TRY(); + { + /* Copy string to PostgreSQL controlled memory */ + cstr = pstrdup((char *) str); + } + PG_CATCH(); + { + xmlFree(str); + PG_RE_THROW(); + } + PG_END_TRY(); + + xmlFree(str); + } + else + { + /* Return empty string when tag is empty */ + cstr = pstrdup(""); + } + } + else + { + /* simple case, result is one value */ + cstr = xml_xmlnodetostr(column_xpathobj->nodesetval->nodeTab[0], + xtCxt->xmlerrcxt); + } + } + else + { + StringInfoData str; + int i; + + /* + * When result is not one value, then we can work with + * concated values. But it requires XML as expected + * type. + */ + if (targettypid != XMLOID) + ereport(ERROR, + (errcode(ERRCODE_CARDINALITY_VIOLATION), + errmsg("more than one value returned by column XPath expression"))); + + /* Concate serialized values */ + initStringInfo(&str); + for (i = 0; i < count; i++) + { + appendStringInfoString(&str, + xml_xmlnodetostr(column_xpathobj->nodesetval->nodeTab[i], + xtCxt->xmlerrcxt)); + } + cstr = str.data; + } + + result = InputFunctionCall(&xtCxt->in_functions[colnum], + cstr, + xtCxt->typioparams[colnum], + xtCxt->tupdesc->attrs[colnum]->atttypmod); + *isnull = false; + } + else + *isnull = true; + } + else if (column_xpathobj->type == XPATH_STRING) + { + result = InputFunctionCall(&xtCxt->in_functions[colnum], + (char *) column_xpathobj->stringval, + xtCxt->typioparams[colnum], + xtCxt->tupdesc->attrs[colnum]->atttypmod); + *isnull = false; + } + else + elog(ERROR, "unexpected XPath object type"); + + xmlXPathFreeObject(column_xpathobj); + column_xpathobj = NULL; + } + PG_CATCH(); + { + if (column_xpathobj != NULL) + xmlXPathFreeObject(column_xpathobj); + PG_RE_THROW(); + } + PG_END_TRY(); + } + else + { + /* + * xtCxt->xpathscomp[ncol] is NULL when COLUMNS is empty. Result is a + * serialized current node. + */ + cstr = xml_xmlnodetostr(cur, xtCxt->xmlerrcxt); + result = InputFunctionCall(&xtCxt->in_functions[0], + cstr, + xtCxt->typioparams[0], + -1); /* target type is XML always */ + *isnull = false; + } + + if (cstr != NULL) + pfree(cstr); + + return result; +#else + NO_XML_SUPPORT(); +#endif /* not USE_LIBXML */ +} + +/* + * Release all libxml2 memory and related resources + */ +static void +XmlTableDestroyContext(void *tcontext) +{ +#ifdef USE_LIBXML + XmlTableContext *xtCxt = (XmlTableContext *) tcontext; + + /* Propagate context related error context to libxml2 */ + xmlSetStructuredErrorFunc((void *) xtCxt->xmlerrcxt, xml_errorHandler); + + if (xtCxt->xpathscomp != NULL) + { + int i; + + for (i = 0; i < xtCxt->tupdesc->natts; i++) + if (xtCxt->xpathscomp[i] != NULL) + xmlXPathFreeCompExpr(xtCxt->xpathscomp[i]); + } + + if (xtCxt->xpathobj != NULL) + xmlXPathFreeObject(xtCxt->xpathobj); + if (xtCxt->xpathcomp != NULL) + xmlXPathFreeCompExpr(xtCxt->xpathcomp); + if (xtCxt->xpathcxt != NULL) + xmlXPathFreeContext(xtCxt->xpathcxt); + if (xtCxt->doc != NULL) + xmlFreeDoc(xtCxt->doc); + if (xtCxt->ctxt != NULL) + xmlFreeParserCtxt(xtCxt->ctxt); + + pg_xml_done(xtCxt->xmlerrcxt, true); + +#else + NO_XML_SUPPORT(); +#endif /* not USE_LIBXML */ +} diff --git a/src/backend/utils/adt/xpath_parser.c b/src/backend/utils/adt/xpath_parser.c new file mode 100644 index 0000000..ff8aabf --- /dev/null +++ b/src/backend/utils/adt/xpath_parser.c @@ -0,0 +1,337 @@ +/*------------------------------------------------------------------------- + * + * xpath_parser.c + * XML XPath parser. + * + * Portions Copyright (c) 1996-2016, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/backend/utils/adt/xpath_parser.c + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "utils/xpath_parser.h" + +/* + * All PostgreSQL XML related functionality is based on libxml2 library, and + * XPath support is not an exception. However, libxml2 doesn't provide the + * functions necessary to implement an XMLTABLE function. There is no API to + * access the XPath expression AST (abstract syntax tree), and there is no + * support for default namespaces in XPath expressions. + * + * Those functionalities are implemented with a simple XPath parser/ + * preprocessor. This XPath parser transforms a XPath expression to another + * XPath expression that can be used by libxml2 XPath evaluation. It doesn't + * replace libxml2 XPath parser or libxml2 XPath expression evaluation. + */ + +/* + * support functions for XMLTABLE function + */ +#ifdef USE_LIBXML + +/* + * We need to work with XPath expression tokens. When expression starting with + * nodename, then we can use prefix. When default namespace is defined, then we + * should to enhance any nodename and attribute without namespace by default + * namespace. + */ + +typedef enum +{ + XPATH_TOKEN_NONE, + XPATH_TOKEN_NAME, + XPATH_TOKEN_STRING, + XPATH_TOKEN_NUMBER, + XPATH_TOKEN_OTHER +} XPathTokenType; + +typedef struct TokenInfo +{ + XPathTokenType ttype; + char *start; + int length; +} XPathTokenInfo; + +#define TOKEN_STACK_SIZE 10 + +typedef struct ParserData +{ + char *str; + char *cur; + XPathTokenInfo stack[TOKEN_STACK_SIZE]; + int stack_length; +} XPathParserData; + +/* Any high-bit-set character is OK (might be part of a multibyte char) */ +#define NODENAME_FIRSTCHAR(c) ((c) == '_' || (c) == '-' || \ + ((c) >= 'A' && (c) <= 'Z') || \ + ((c) >= 'a' && (c) <= 'z') || \ + (IS_HIGHBIT_SET(c))) + +#define IS_NODENAME_CHAR(c) (NODENAME_FIRSTCHAR(c) || (c) == '.' || \ + ((c) >= '0' && (c) <= '9')) + + +/* + * Returns next char after last char of token + */ +static char * +getXPathToken(char *str, XPathTokenInfo * ti) +{ + /* skip initial spaces */ + while (*str == ' ') + str++; + + if (*str != '\0') + { + char c = *str; + + ti->start = str++; + + if (c >= '0' && c <= '9') + { + while (*str >= '0' && *str <= '9') + str++; + if (*str == '.') + { + str++; + while (*str >= '0' && *str <= '9') + str++; + } + ti->ttype = XPATH_TOKEN_NUMBER; + } + else if (NODENAME_FIRSTCHAR(c)) + { + while (IS_NODENAME_CHAR(*str)) + str++; + + ti->ttype = XPATH_TOKEN_NAME; + } + else if (c == '"') + { + while (*str != '\0') + if (*str++ == '"') + break; + + ti->ttype = XPATH_TOKEN_STRING; + } + else + ti->ttype = XPATH_TOKEN_OTHER; + + ti->length = str - ti->start; + } + else + { + ti->start = NULL; + ti->length = 0; + + ti->ttype = XPATH_TOKEN_NONE; + } + + return str; +} + +/* + * reset XPath parser stack + */ +static void +initXPathParser(XPathParserData * parser, char *str) +{ + parser->str = str; + parser->cur = str; + parser->stack_length = 0; +} + +/* + * Returns token from stack or read token + */ +static void +nextXPathToken(XPathParserData * parser, XPathTokenInfo * ti) +{ + if (parser->stack_length > 0) + memcpy(ti, &parser->stack[--parser->stack_length], + sizeof(XPathTokenInfo)); + else + parser->cur = getXPathToken(parser->cur, ti); +} + +/* + * Push token to stack + */ +static void +pushXPathToken(XPathParserData * parser, XPathTokenInfo * ti) +{ + if (parser->stack_length == TOKEN_STACK_SIZE) + elog(ERROR, "internal error"); + memcpy(&parser->stack[parser->stack_length++], ti, + sizeof(XPathTokenInfo)); +} + +/* + * Write token to output string + */ +static void +writeXPathToken(StringInfo str, XPathTokenInfo * ti) +{ + Assert(ti->ttype != XPATH_TOKEN_NONE); + + if (ti->ttype != XPATH_TOKEN_OTHER) + appendBinaryStringInfo(str, ti->start, ti->length); + else + appendStringInfoChar(str, *ti->start); +} + +/* + * Working horse for XPath transformation. When XPath starting by node name, + * then prefix have to be applied. Any unqualified node name should be + * qualified by default namespace. inside_predicate is true, when + * _transformXPath is recursivly called because the predicate expression + * was found. + */ +static void +_transformXPath(StringInfo str, XPathParserData * parser, + bool inside_predicate, + char *prefix, char *def_namespace_name) +{ + XPathTokenInfo t1, + t2; + bool is_first_token = true; + bool last_token_is_name = false; + + nextXPathToken(parser, &t1); + + while (t1.ttype != XPATH_TOKEN_NONE) + { + switch (t1.ttype) + { + case XPATH_TOKEN_NUMBER: + case XPATH_TOKEN_STRING: + last_token_is_name = false; + is_first_token = false; + writeXPathToken(str, &t1); + nextXPathToken(parser, &t1); + break; + + case XPATH_TOKEN_NAME: + { + bool is_qual_name = false; + + /* inside predicate ignore keywords "and" "or" */ + if (inside_predicate) + { + if ((strncmp(t1.start, "and", 3) == 0 && t1.length == 3) || + (strncmp(t1.start, "or", 2) == 0 && t1.length == 2)) + { + writeXPathToken(str, &t1); + nextXPathToken(parser, &t1); + break; + } + } + + last_token_is_name = true; + nextXPathToken(parser, &t2); + if (t2.ttype == XPATH_TOKEN_OTHER) + { + if (*t2.start == '(') + last_token_is_name = false; + else if (*t2.start == ':') + is_qual_name = true; + } + + if (is_first_token && last_token_is_name && prefix != NULL) + appendStringInfoString(str, prefix); + + if (last_token_is_name && !is_qual_name && def_namespace_name != NULL) + appendStringInfo(str, "%s:", def_namespace_name); + + writeXPathToken(str, &t1); + is_first_token = false; + + if (is_qual_name) + { + writeXPathToken(str, &t2); + nextXPathToken(parser, &t1); + if (t1.ttype == XPATH_TOKEN_NAME) + writeXPathToken(str, &t1); + else + pushXPathToken(parser, &t1); + } + else + pushXPathToken(parser, &t2); + + nextXPathToken(parser, &t1); + } + break; + + case XPATH_TOKEN_OTHER: + { + char c = *t1.start; + + is_first_token = false; + + writeXPathToken(str, &t1); + + if (c == '[') + _transformXPath(str, parser, true, NULL, def_namespace_name); + else + { + last_token_is_name = false; + + if (c == ']' && inside_predicate) + return; + + else if (c == '@') + { + nextXPathToken(parser, &t1); + if (t1.ttype == XPATH_TOKEN_NAME) + { + bool is_qual_name = false; + + nextXPathToken(parser, &t2); + if (t2.ttype == XPATH_TOKEN_OTHER && *t2.start == ':') + is_qual_name = true; + + if (!is_qual_name && def_namespace_name != NULL) + appendStringInfo(str, "%s:", def_namespace_name); + + writeXPathToken(str, &t1); + if (is_qual_name) + { + writeXPathToken(str, &t2); + nextXPathToken(parser, &t1); + if (t1.ttype == XPATH_TOKEN_NAME) + writeXPathToken(str, &t1); + else + pushXPathToken(parser, &t1); + } + else + pushXPathToken(parser, &t2); + } + else + pushXPathToken(parser, &t1); + } + } + nextXPathToken(parser, &t1); + } + break; + + case XPATH_TOKEN_NONE: + elog(ERROR, "should not be here"); + } + } +} + +void +transformXPath(StringInfo str, char *xpath, + char *prefix, char *def_namespace_name) +{ + XPathParserData parser; + + initStringInfo(str); + initXPathParser(&parser, xpath); + _transformXPath(str, &parser, false, prefix, def_namespace_name); +} + +#endif diff --git a/src/backend/utils/fmgr/funcapi.c b/src/backend/utils/fmgr/funcapi.c index 5d49fe5..979f3f8 100644 --- a/src/backend/utils/fmgr/funcapi.c +++ b/src/backend/utils/fmgr/funcapi.c @@ -241,6 +241,19 @@ get_expr_result_type(Node *expr, NULL, resultTypeId, resultTupleDesc); + else if (expr && IsA(expr, TableExpr)) + { + Oid typid = exprType(expr); + int32 typmod = exprTypmod(expr); + + if (resultTypeId) + *resultTypeId = typid; + + if (resultTupleDesc) + *resultTupleDesc = lookup_rowtype_tupdesc_copy(typid, typmod); + + return TYPEFUNC_COMPOSITE; + } else { /* handle as a generic expression; no chance to resolve RECORD */ diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index 136276b..c6d69bc 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -272,6 +272,7 @@ extern TupleTableSlot *ExecInitNullTupleSlot(EState *estate, extern TupleDesc ExecTypeFromTL(List *targetList, bool hasoid); extern TupleDesc ExecCleanTypeFromTL(List *targetList, bool hasoid); extern TupleDesc ExecTypeFromExprList(List *exprList); +extern TupleDesc ExecTypeFromTableExpr(const TableExpr *te); extern void ExecTypeSetColNames(TupleDesc typeInfo, List *namesList); extern void UpdateChangedParamSet(PlanState *node, Bitmapset *newchg); diff --git a/src/include/executor/tableexpr.h b/src/include/executor/tableexpr.h new file mode 100644 index 0000000..12791ab --- /dev/null +++ b/src/include/executor/tableexpr.h @@ -0,0 +1,69 @@ +/*------------------------------------------------------------------------- + * + * tableexpr.h + * interface for TableExpr builder + * + * Portions Copyright (c) 1996-2016, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/tableexpr.h + * + *------------------------------------------------------------------------- + */ +#ifndef TABLEEXPR_H +#define TABLEEXPR_H + +#include "nodes/primnodes.h" + +/* + * This structure holds a collection of function pointers used + * for generating content of table-expression functions like + * XMLTABLE. + * + * The TableBuilder is initialized by calling CreateContext function + * at evaluation time. First parameter - tuple descriptor describes + * produced (expected) table. in_functions is a array of FmgrInfo input + * functions for types of columns of produced table. The typioparams + * is a array of typio Oids for types of columns of produced table. + * The created context is living in special memory context passed + * as last parameter. + * + * The SetContent function is used for passing input document to + * table builder. The table builder handler knows expected format + * and it can do some additional transformations that are not propagated + * out from table builder. + * + * The AddNs add namespace info when namespaces are supported. + * Namespaces should be passed before Row/Column Paths setting. + * When name is NULL, then related uri is default namespace. + * + * The SetRowPath sets a row generating filter. This filter is used + * for separation of rows from document. Passed as cstring. + * + * The SetColumnPath sets a column generating filter. This filter is + * used for separating nth value from row. Passed as cstring. + * + * The FetchRow ensure loading row raleted data. Returns false, when + * document doesn't containt any next row. + * + * The GetValue returns a value related to colnum column. + * + * The DestroyContext - should to release all sources related to + * processing the document. Called when all rows are fetched or + * when a error is catched. + */ +typedef struct TableExprBuilder +{ + void *(*CreateContext) (TupleDesc tupdesc, + FmgrInfo *in_functions, Oid *typioparams, + MemoryContext per_rowset_memory); + void (*SetContent) (void *tcontext, Datum value); + void (*AddNS) (void *tcontext, char *name, char *uri); + void (*SetRowPath) (void *tcontext, char *path); + void (*SetColumnPath) (void *tcontext, char *path, int colnum); + bool (*FetchRow) (void *tcontext); + Datum (*GetValue) (void *tcontext, int colnum, bool *isnull); + void (*DestroyContext) (void *tcontext); +} TableExprBuilder; + +#endif /* TABLEEXPR_H */ diff --git a/src/include/funcapi.h b/src/include/funcapi.h index e73a824..e350316 100644 --- a/src/include/funcapi.h +++ b/src/include/funcapi.h @@ -184,7 +184,6 @@ extern TupleDesc build_function_result_tupdesc_d(Datum proallargtypes, Datum proargnames); extern TupleDesc build_function_result_tupdesc_t(HeapTuple procTuple); - /*---------- * Support to ease writing functions returning composite types * diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index f6f73f3..523afc8 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -17,6 +17,7 @@ #include "access/genam.h" #include "access/heapam.h" #include "executor/instrument.h" +#include "executor/tableexpr.h" #include "lib/pairingheap.h" #include "nodes/params.h" #include "nodes/plannodes.h" @@ -1011,6 +1012,36 @@ typedef struct DomainConstraintState ExprState *check_expr; /* for CHECK, a boolean expression */ } DomainConstraintState; +/* ---------------- + * TableExprState node + * + * Used in table-expression functions like XMLTABLE. + * ---------------- + */ +typedef struct TableExprState +{ + ExprState xprstate; + List *namespaces; /* list of prepared ResTarget fields */ + TupleDesc tupdesc; /* cache */ + int ncols; /* number of declared columns */ + int for_ordinality_col; /* number of oridinality column, + * started by 1 */ + int rownum; /* row counter - for ordinality column */ + ExprState *row_path_expr; /* row xpath expression */ + ExprState *expr; /* processed data */ + ExprState **def_expr; /* array of expressions for default value */ + ExprState **col_path_expr; /* array of expressions for path value */ + bool *not_null; /* for any column info if NULL is allowed or + * not */ + Datum *values; /* prealloc buffer */ + bool *nulls; /* prealloc buffer */ + FmgrInfo *in_functions; /* array of infunction for any column */ + Oid *typioparams; /* array of typIOParam for any column */ + const TableExprBuilder *builder; /* pointers to builder functions */ + void *builderCxt; /* data for content builder */ + + MemoryContext per_rowset_memory; +} TableExprState; /* ---------------------------------------------------------------- * Executor State Trees diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index cb9307c..b6e5151 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -181,6 +181,8 @@ typedef enum NodeTag T_FromExpr, T_OnConflictExpr, T_IntoClause, + T_TableExpr, + T_TableExprColumn, /* * TAGS FOR EXPRESSION STATE NODES (execnodes.h) @@ -216,6 +218,7 @@ typedef enum NodeTag T_NullTestState, T_CoerceToDomainState, T_DomainConstraintState, + T_TableExprState, /* * TAGS FOR PLANNER NODES (relation.h) @@ -454,6 +457,7 @@ typedef enum NodeTag T_CommonTableExpr, T_RoleSpec, T_TriggerTransition, + T_TableExprRawCol, /* * TAGS FOR REPLICATION GRAMMAR PARSE NODES (replnodes.h) diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 04b1c2f..21859cc 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -699,6 +699,27 @@ typedef struct XmlSerialize int location; /* token location, or -1 if unknown */ } XmlSerialize; +/* + * TableExprRawCol - raw column definition in table-expression functions + * like XMLTABLE. + * + * We can't re-use ColumnDef here; the utility command column + * definition has different set of attributes than table-expressions. + * + * If for_ordinality is true (FOR ORDINALITY), then the column is an int4 + * column and the rest of the fields are ignored. + */ +typedef struct TableExprRawCol +{ + NodeTag type; + char *colname; + bool for_ordinality; + TypeName *typeName; + bool is_not_null; + Node *path_expr; + Node *default_expr; + int location; +} TableExprRawCol; /**************************************************************************** * Nodes for a Query tree diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 65510b0..3fcf546 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1467,4 +1467,44 @@ typedef struct OnConflictExpr List *exclRelTlist; /* tlist of the EXCLUDED pseudo relation */ } OnConflictExpr; +/*---------- + * TableExpr - holds data for table-expression functions like XMLTABLE + * + *---------- + */ +typedef struct TableExpr +{ + NodeTag type; + Node *row_path; /* row xpath query */ + Node *expr; /* processed data */ + List *cols; /* columns definitions */ + List *namespaces; /* list of namespaces */ + int location; +} TableExpr; + +/*---------- + * TableExprColumn - a column definition in table-expression functions. + * + * We can't re-use ColumnDef here; the utility command column + * definition has different set of attributes than table-expressions + * and just doesn't make sense here. + * + * Raw form of this node is TableExprRawCol node - different in + * typeName field. After transformation we use typid, typmod. + *---------- + */ +typedef struct TableExprColumn +{ + NodeTag type; + char *colname; + Oid typid; + int32 typmod; + Oid collation; + bool for_ordinality; + bool is_not_null; + Node *path_expr; + Node *default_expr; + int location; +} TableExprColumn; + #endif /* PRIMNODES_H */ diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 77d873b..de4b8b8 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -81,6 +81,7 @@ PG_KEYWORD("coalesce", COALESCE, COL_NAME_KEYWORD) PG_KEYWORD("collate", COLLATE, RESERVED_KEYWORD) PG_KEYWORD("collation", COLLATION, TYPE_FUNC_NAME_KEYWORD) PG_KEYWORD("column", COLUMN, RESERVED_KEYWORD) +PG_KEYWORD("columns", COLUMNS, UNRESERVED_KEYWORD) PG_KEYWORD("comment", COMMENT, UNRESERVED_KEYWORD) PG_KEYWORD("comments", COMMENTS, UNRESERVED_KEYWORD) PG_KEYWORD("commit", COMMIT, UNRESERVED_KEYWORD) @@ -441,10 +442,12 @@ PG_KEYWORD("xmlconcat", XMLCONCAT, COL_NAME_KEYWORD) PG_KEYWORD("xmlelement", XMLELEMENT, COL_NAME_KEYWORD) PG_KEYWORD("xmlexists", XMLEXISTS, COL_NAME_KEYWORD) PG_KEYWORD("xmlforest", XMLFOREST, COL_NAME_KEYWORD) +PG_KEYWORD("xmlnamespaces", XMLNAMESPACES, COL_NAME_KEYWORD) PG_KEYWORD("xmlparse", XMLPARSE, COL_NAME_KEYWORD) PG_KEYWORD("xmlpi", XMLPI, COL_NAME_KEYWORD) PG_KEYWORD("xmlroot", XMLROOT, COL_NAME_KEYWORD) PG_KEYWORD("xmlserialize", XMLSERIALIZE, COL_NAME_KEYWORD) +PG_KEYWORD("xmltable", XMLTABLE, COL_NAME_KEYWORD) PG_KEYWORD("year", YEAR_P, UNRESERVED_KEYWORD) PG_KEYWORD("yes", YES_P, UNRESERVED_KEYWORD) PG_KEYWORD("zone", ZONE, UNRESERVED_KEYWORD) diff --git a/src/include/parser/parse_coerce.h b/src/include/parser/parse_coerce.h index 66519e6..03502f8 100644 --- a/src/include/parser/parse_coerce.h +++ b/src/include/parser/parse_coerce.h @@ -58,6 +58,10 @@ extern Node *coerce_to_specific_type(ParseState *pstate, Node *node, Oid targetTypeId, const char *constructName); +extern Node *coerce_to_specific_type_typmod(ParseState *pstate, Node *node, + Oid targetTypeId, int32 targetTypmod, + const char *constructName); + extern int parser_coercion_errposition(ParseState *pstate, int coerce_location, Node *input_expr); diff --git a/src/include/utils/xml.h b/src/include/utils/xml.h index 2eab8a5..1229b76 100644 --- a/src/include/utils/xml.h +++ b/src/include/utils/xml.h @@ -109,4 +109,6 @@ extern int xmlbinary; /* XmlBinaryType, but int for guc enum */ extern int xmloption; /* XmlOptionType, but int for guc enum */ +extern const TableExprBuilder XmlTableBuilder; + #endif /* XML_H */ diff --git a/src/include/utils/xpath_parser.h b/src/include/utils/xpath_parser.h new file mode 100644 index 0000000..c6fc532 --- /dev/null +++ b/src/include/utils/xpath_parser.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * xpath_parser.h + * Declarations for XML XPath transformation. + * + * + * Portions Copyright (c) 1996-2016, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/utils/xml.h + * + *------------------------------------------------------------------------- + */ + +#ifndef XPATH_PARSER_H +#define XPATH_PARSER_H + +#include "postgres.h" +#include "lib/stringinfo.h" + +void transformXPath(StringInfo str, char *xpath, + char *prefix, char *def_namespace_name); + +#endif /* XPATH_PARSER_H */ diff --git a/src/test/regress/expected/xml.out b/src/test/regress/expected/xml.out index f21e119..3f14b3b 100644 --- a/src/test/regress/expected/xml.out +++ b/src/test/regress/expected/xml.out @@ -948,3 +948,418 @@ SELECT XMLPARSE(DOCUMENT '  (1 row) +-- XMLPATH tests +CREATE TABLE xmldata(data xml); +INSERT INTO xmldata VALUES(' + + AU + Australia + 3 + + + CN + China + 3 + + + HK + HongKong + 3 + + + IN + India + 3 + + + JP + Japan + 3Sinzo Abe + + + SG + Singapore + 3791 + +'); +-- XMLTABLE without columns +SELECT * FROM XMLTABLE('/rows/row' PASSING '1020'); + xmltable +------------- + + + 10+ + 20+ + +(1 row) + +SELECT XMLTABLE('/rows/row' PASSING '1020'); + xmltable +------------- + (" + + 10+ + 20+ + ") +(1 row) + +-- XMLTABLE with columns +SELECT xmltable.* + FROM (SELECT data FROM xmldata) x, + LATERAL XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); + id | _id | country_name | country_id | region_id | size | unit | premier_name +----+-----+--------------+------------+-----------+------+------+--------------- + 1 | 1 | Australia | AU | 3 | | | not specified + 2 | 2 | China | CN | 3 | | | not specified + 3 | 3 | HongKong | HK | 3 | | | not specified + 4 | 4 | India | IN | 3 | | | not specified + 5 | 5 | Japan | JP | 3 | | | Sinzo Abe + 6 | 6 | Singapore | SG | 3 | 791 | km | not specified +(6 rows) + +CREATE VIEW xmltableview1 AS SELECT xmltable.* + FROM (SELECT data FROM xmldata) x, + LATERAL XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); +SELECT * FROM xmltableview1; + id | _id | country_name | country_id | region_id | size | unit | premier_name +----+-----+--------------+------------+-----------+------+------+--------------- + 1 | 1 | Australia | AU | 3 | | | not specified + 2 | 2 | China | CN | 3 | | | not specified + 3 | 3 | HongKong | HK | 3 | | | not specified + 4 | 4 | India | IN | 3 | | | not specified + 5 | 5 | Japan | JP | 3 | | | Sinzo Abe + 6 | 6 | Singapore | SG | 3 | 791 | km | not specified +(6 rows) + + +\sv xmltableview1 +CREATE OR REPLACE VIEW public.xmltableview1 AS + SELECT "xmltable".id, + "xmltable"._id, + "xmltable".country_name, + "xmltable".country_id, + "xmltable".region_id, + "xmltable".size, + "xmltable".unit, + "xmltable".premier_name + FROM ( SELECT xmldata.data + FROM xmldata) x, + LATERAL XMLTABLE(('/ROWS/ROW'::text) PASSING (x.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) "xmltable"(id, _id, country_name, country_id, region_id, size, unit, premier_name) +EXPLAIN (COSTS OFF) SELECT * FROM xmltableview1; + QUERY PLAN +----------------------------------- + Nested Loop + -> Seq Scan on xmldata + -> Function Scan on "xmltable" +(3 rows) + +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM xmltableview1; + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Nested Loop + Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name + -> Seq Scan on public.xmldata + Output: xmldata.data + -> Function Scan on "xmltable" + Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name + Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) +(7 rows) + +-- XMLNAMESPACES tests +SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS zz), + '/zz:rows/zz:row' + PASSING '10' + COLUMNS a int PATH 'zz:a'); + a +---- + 10 +(1 row) + +CREATE VIEW xmltableview2 AS SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS zz), + '/zz:rows/zz:row' + PASSING '10' + COLUMNS a int PATH 'zz:a'); +SELECT * FROM xmltableview2; + a +---- + 10 +(1 row) + +SELECT * FROM XMLTABLE(XMLNAMESPACES(DEFAULT 'http://x.y'), + '/rows/row' + PASSING '10' + COLUMNS a int PATH 'a'); + a +---- + 10 +(1 row) + +SELECT * FROM XMLTABLE('/rows/row/a/text()' PASSING '1020'); + xmltable +---------- + 10 + 20 +(2 rows) + +-- used in prepare statements +PREPARE pp AS +SELECT xmltable.* + FROM (SELECT data FROM xmldata) x, + LATERAL XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); +EXECUTE pp; + id | _id | country_name | country_id | region_id | size | unit | premier_name +----+-----+--------------+------------+-----------+------+------+--------------- + 1 | 1 | Australia | AU | 3 | | | not specified + 2 | 2 | China | CN | 3 | | | not specified + 3 | 3 | HongKong | HK | 3 | | | not specified + 4 | 4 | India | IN | 3 | | | not specified + 5 | 5 | Japan | JP | 3 | | | Sinzo Abe + 6 | 6 | Singapore | SG | 3 | 791 | km | not specified +(6 rows) + +SELECT xmltable('/ROWS/ROW' PASSING data) FROM xmldata; + xmltable +------------------------------------------------------------------ + (" + + AU + + Australia + + 3 + + ") + (" + + CN + + China + + 3 + + ") + (" + + HK + + HongKong + + 3 + + ") + (" + + IN + + India + + 3 + + ") + (" + + JP + + Japan + + 3Sinzo Abe+ + ") + (" + + SG + + Singapore + + 3791 + + ") +(6 rows) + +SELECT xmltable('/ROWS/ROW[COUNTRY_NAME="Japan"]' PASSING data) FROM xmldata; + xmltable +------------------------------------------------------------------ + (" + + JP + + Japan + + 3Sinzo Abe+ + ") +(1 row) + +SELECT xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data) FROM xmldata; + xmltable +------------------------------------------------------------------ + (" + + IN + + India + + 3 + + ") + (" + + JP + + Japan + + 3Sinzo Abe+ + ") +(2 rows) + +SELECT (xmltable).* FROM (SELECT xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) FROM xmldata) s; + COUNTRY_NAME | REGION_ID +--------------+----------- + India | 3 + Japan | 3 +(2 rows) + +SELECT xmltable.* FROM xmldata,LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int); + COUNTRY_NAME | REGION_ID +--------------+----------- + India | 3 + Japan | 3 +(2 rows) + +SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id FOR ORDINALITY, "COUNTRY_NAME" text, "REGION_ID" int); + id | COUNTRY_NAME | REGION_ID +----+--------------+----------- + 1 | India | 3 + 2 | Japan | 3 +(2 rows) + +SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int); + id | COUNTRY_NAME | REGION_ID +----+--------------+----------- + 4 | India | 3 + 5 | Japan | 3 +(2 rows) + +SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id'); + id +---- + 4 + 5 +(2 rows) + +SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id FOR ORDINALITY); + id +---- + 1 + 2 +(2 rows) + +SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int, rawdata xml PATH '.'); + id | COUNTRY_NAME | REGION_ID | rawdata +----+--------------+-----------+------------------------------------------------------------------ + 4 | India | 3 | + + | | | IN + + | | | India + + | | | 3 + + | | | + 5 | Japan | 3 | + + | | | JP + + | | | Japan + + | | | 3Sinzo Abe+ + | | | +(2 rows) + +SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int, rawdata xml PATH './*'); + id | COUNTRY_NAME | REGION_ID | rawdata +----+--------------+-----------+----------------------------------------------------------------------------------------------------------------------------- + 4 | India | 3 | INIndia3 + 5 | Japan | 3 | JPJapan3Sinzo Abe +(2 rows) + +SELECT * FROM xmltable('/root' passing 'a1aa2a bbbbxxxcccc' COLUMNS element text); + element +------------------- + a1aa2a bbbbcccc +(1 row) + +SELECT * FROM xmltable('/root' passing 'a1aa2a bbbbxxxcccc' COLUMNS element text PATH 'element/text()'); -- should fail +ERROR: more than one value returned by column XPath expression +-- CDATA test +select * from xmltable('r' passing ' &"<>!foo]]>2' columns c text); + c +------------------------- + &"<>!foo + 2 +(2 rows) + +-- XML builtin entities +SELECT * FROM xmltable('/x/a' PASSING ''"&<>' COLUMNS ent text); + ent +----- + ' + " + & + < + > +(5 rows) + +SELECT * FROM xmltable('/x/a' PASSING ''"&<>' COLUMNS ent xml); + ent +------------------ + ' + " + & + < + > +(5 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT xmltable.* + FROM (SELECT data FROM xmldata) x, + LATERAL XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Nested Loop + Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name + -> Seq Scan on public.xmldata + Output: xmldata.data + -> Function Scan on "xmltable" + Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name + Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) +(7 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified') FROM xmldata; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Seq Scan on public.xmldata + Output: XMLTABLE(('/ROWS/ROW'::text) PASSING (data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT (xmltable).* FROM + (SELECT XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified') FROM xmldata) s; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Subquery Scan on s + Output: (s."xmltable").id, (s."xmltable")._id, (s."xmltable").country_name, (s."xmltable").country_id, (s."xmltable").region_id, (s."xmltable").size, (s."xmltable").unit, (s."xmltable").premier_name + -> Seq Scan on public.xmldata + Output: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) +(4 rows) + diff --git a/src/test/regress/expected/xml_1.out b/src/test/regress/expected/xml_1.out index d702703..8987558 100644 --- a/src/test/regress/expected/xml_1.out +++ b/src/test/regress/expected/xml_1.out @@ -827,3 +827,326 @@ SELECT XMLPARSE(DOCUMENT ' + + AU + Australia + 3 + + + CN + China + 3 + + + HK + HongKong + 3 + + + IN + India + 3 + + + JP + Japan + 3Sinzo Abe + + + SG + Singapore + 3791 + +'); +ERROR: unsupported XML feature +LINE 1: INSERT INTO xmldata VALUES(' + ^ +DETAIL: This functionality requires the server to be built with libxml support. +HINT: You need to rebuild PostgreSQL using --with-libxml. +-- XMLTABLE without columns +SELECT * FROM XMLTABLE('/rows/row' PASSING '1020'); +ERROR: unsupported XML feature +LINE 1: SELECT * FROM XMLTABLE('/rows/row' PASSING '10... + ^ +DETAIL: This functionality requires the server to be built with libxml support. +HINT: You need to rebuild PostgreSQL using --with-libxml. +SELECT XMLTABLE('/rows/row' PASSING '1020'); +ERROR: unsupported XML feature +LINE 1: SELECT XMLTABLE('/rows/row' PASSING '10... + ^ +DETAIL: This functionality requires the server to be built with libxml support. +HINT: You need to rebuild PostgreSQL using --with-libxml. +-- XMLTABLE with columns +SELECT xmltable.* + FROM (SELECT data FROM xmldata) x, + LATERAL XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); + id | _id | country_name | country_id | region_id | size | unit | premier_name +----+-----+--------------+------------+-----------+------+------+-------------- +(0 rows) + +CREATE VIEW xmltableview1 AS SELECT xmltable.* + FROM (SELECT data FROM xmldata) x, + LATERAL XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); +SELECT * FROM xmltableview1; + id | _id | country_name | country_id | region_id | size | unit | premier_name +----+-----+--------------+------------+-----------+------+------+-------------- +(0 rows) + +\sv xmltableview1 +CREATE OR REPLACE VIEW public.xmltableview1 AS + SELECT "xmltable".id, + "xmltable"._id, + "xmltable".country_name, + "xmltable".country_id, + "xmltable".region_id, + "xmltable".size, + "xmltable".unit, + "xmltable".premier_name + FROM ( SELECT xmldata.data + FROM xmldata) x, + LATERAL XMLTABLE(('/ROWS/ROW'::text) PASSING (x.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) "xmltable"(id, _id, country_name, country_id, region_id, size, unit, premier_name) +EXPLAIN (COSTS OFF) SELECT * FROM xmltableview1; + QUERY PLAN +----------------------------------- + Nested Loop + -> Seq Scan on xmldata + -> Function Scan on "xmltable" +(3 rows) + +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM xmltableview1; + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Nested Loop + Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name + -> Seq Scan on public.xmldata + Output: xmldata.data + -> Function Scan on "xmltable" + Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name + Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) +(7 rows) + +-- XMLNAMESPACES tests +SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS zz), + '/zz:rows/zz:row' + PASSING '10' + COLUMNS a int PATH 'zz:a'); +ERROR: unsupported XML feature +LINE 3: PASSING '10' + COLUMNS a int PATH 'zz:a'); +ERROR: unsupported XML feature +LINE 3: PASSING '10' + COLUMNS a int PATH 'a'); +ERROR: unsupported XML feature +LINE 3: PASSING '1020'); +ERROR: unsupported XML feature +LINE 1: ...LECT * FROM XMLTABLE('/rows/row/a/text()' PASSING 'a1aa2a bbbbxxxcccc' COLUMNS element text); +ERROR: unsupported XML feature +LINE 1: SELECT * FROM xmltable('/root' passing 'a1aa1aa2a bbbbxxxcccc' COLUMNS element text PATH 'element/text()'); -- should fail +ERROR: unsupported XML feature +LINE 1: SELECT * FROM xmltable('/root' passing 'a1a &"<>!foo]]>2' columns c text); +ERROR: unsupported XML feature +LINE 1: select * from xmltable('r' passing ''"&<>' COLUMNS ent text); +ERROR: unsupported XML feature +LINE 1: SELECT * FROM xmltable('/x/a' PASSING '''"&<>' COLUMNS ent xml); +ERROR: unsupported XML feature +LINE 1: SELECT * FROM xmltable('/x/a' PASSING '' Seq Scan on public.xmldata + Output: xmldata.data + -> Function Scan on "xmltable" + Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name + Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) +(7 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified') FROM xmldata; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Seq Scan on public.xmldata + Output: XMLTABLE(('/ROWS/ROW'::text) PASSING (data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT (xmltable).* FROM + (SELECT XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified') FROM xmldata) s; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Subquery Scan on s + Output: (s."xmltable").id, (s."xmltable")._id, (s."xmltable").country_name, (s."xmltable").country_id, (s."xmltable").region_id, (s."xmltable").size, (s."xmltable").unit, (s."xmltable").premier_name + -> Seq Scan on public.xmldata + Output: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) +(4 rows) + diff --git a/src/test/regress/expected/xml_2.out b/src/test/regress/expected/xml_2.out index 530faf5..f591be3 100644 --- a/src/test/regress/expected/xml_2.out +++ b/src/test/regress/expected/xml_2.out @@ -928,3 +928,417 @@ SELECT XMLPARSE(DOCUMENT '  (1 row) +-- XMLPATH tests +CREATE TABLE xmldata(data xml); +INSERT INTO xmldata VALUES(' + + AU + Australia + 3 + + + CN + China + 3 + + + HK + HongKong + 3 + + + IN + India + 3 + + + JP + Japan + 3Sinzo Abe + + + SG + Singapore + 3791 + +'); +-- XMLTABLE without columns +SELECT * FROM XMLTABLE('/rows/row' PASSING '1020'); + xmltable +------------- + + + 10+ + 20+ + +(1 row) + +SELECT XMLTABLE('/rows/row' PASSING '1020'); + xmltable +------------- + (" + + 10+ + 20+ + ") +(1 row) + +-- XMLTABLE with columns +SELECT xmltable.* + FROM (SELECT data FROM xmldata) x, + LATERAL XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); + id | _id | country_name | country_id | region_id | size | unit | premier_name +----+-----+--------------+------------+-----------+------+------+--------------- + 1 | 1 | Australia | AU | 3 | | | not specified + 2 | 2 | China | CN | 3 | | | not specified + 3 | 3 | HongKong | HK | 3 | | | not specified + 4 | 4 | India | IN | 3 | | | not specified + 5 | 5 | Japan | JP | 3 | | | Sinzo Abe + 6 | 6 | Singapore | SG | 3 | 791 | km | not specified +(6 rows) + +CREATE VIEW xmltableview1 AS SELECT xmltable.* + FROM (SELECT data FROM xmldata) x, + LATERAL XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); +SELECT * FROM xmltableview1; + id | _id | country_name | country_id | region_id | size | unit | premier_name +----+-----+--------------+------------+-----------+------+------+--------------- + 1 | 1 | Australia | AU | 3 | | | not specified + 2 | 2 | China | CN | 3 | | | not specified + 3 | 3 | HongKong | HK | 3 | | | not specified + 4 | 4 | India | IN | 3 | | | not specified + 5 | 5 | Japan | JP | 3 | | | Sinzo Abe + 6 | 6 | Singapore | SG | 3 | 791 | km | not specified +(6 rows) + +\sv xmltableview1 +CREATE OR REPLACE VIEW public.xmltableview1 AS + SELECT "xmltable".id, + "xmltable"._id, + "xmltable".country_name, + "xmltable".country_id, + "xmltable".region_id, + "xmltable".size, + "xmltable".unit, + "xmltable".premier_name + FROM ( SELECT xmldata.data + FROM xmldata) x, + LATERAL XMLTABLE(('/ROWS/ROW'::text) PASSING (x.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) "xmltable"(id, _id, country_name, country_id, region_id, size, unit, premier_name) +EXPLAIN (COSTS OFF) SELECT * FROM xmltableview1; + QUERY PLAN +----------------------------------- + Nested Loop + -> Seq Scan on xmldata + -> Function Scan on "xmltable" +(3 rows) + +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM xmltableview1; + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Nested Loop + Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name + -> Seq Scan on public.xmldata + Output: xmldata.data + -> Function Scan on "xmltable" + Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name + Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) +(7 rows) + +-- XMLNAMESPACES tests +SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS zz), + '/zz:rows/zz:row' + PASSING '10' + COLUMNS a int PATH 'zz:a'); + a +---- + 10 +(1 row) + +CREATE VIEW xmltableview2 AS SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS zz), + '/zz:rows/zz:row' + PASSING '10' + COLUMNS a int PATH 'zz:a'); +SELECT * FROM xmltableview2; + a +---- + 10 +(1 row) + +SELECT * FROM XMLTABLE(XMLNAMESPACES(DEFAULT 'http://x.y'), + '/rows/row' + PASSING '10' + COLUMNS a int PATH 'a'); + a +---- + 10 +(1 row) + +SELECT * FROM XMLTABLE('/rows/row/a/text()' PASSING '1020'); + xmltable +---------- + 10 + 20 +(2 rows) + +-- used in prepare statements +PREPARE pp AS +SELECT xmltable.* + FROM (SELECT data FROM xmldata) x, + LATERAL XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); +EXECUTE pp; + id | _id | country_name | country_id | region_id | size | unit | premier_name +----+-----+--------------+------------+-----------+------+------+--------------- + 1 | 1 | Australia | AU | 3 | | | not specified + 2 | 2 | China | CN | 3 | | | not specified + 3 | 3 | HongKong | HK | 3 | | | not specified + 4 | 4 | India | IN | 3 | | | not specified + 5 | 5 | Japan | JP | 3 | | | Sinzo Abe + 6 | 6 | Singapore | SG | 3 | 791 | km | not specified +(6 rows) + +SELECT xmltable('/ROWS/ROW' PASSING data) FROM xmldata; + xmltable +------------------------------------------------------------------ + (" + + AU + + Australia + + 3 + + ") + (" + + CN + + China + + 3 + + ") + (" + + HK + + HongKong + + 3 + + ") + (" + + IN + + India + + 3 + + ") + (" + + JP + + Japan + + 3Sinzo Abe+ + ") + (" + + SG + + Singapore + + 3791 + + ") +(6 rows) + +SELECT xmltable('/ROWS/ROW[COUNTRY_NAME="Japan"]' PASSING data) FROM xmldata; + xmltable +------------------------------------------------------------------ + (" + + JP + + Japan + + 3Sinzo Abe+ + ") +(1 row) + +SELECT xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data) FROM xmldata; + xmltable +------------------------------------------------------------------ + (" + + IN + + India + + 3 + + ") + (" + + JP + + Japan + + 3Sinzo Abe+ + ") +(2 rows) + +SELECT (xmltable).* FROM (SELECT xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) FROM xmldata) s; + COUNTRY_NAME | REGION_ID +--------------+----------- + India | 3 + Japan | 3 +(2 rows) + +SELECT xmltable.* FROM xmldata,LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int); + COUNTRY_NAME | REGION_ID +--------------+----------- + India | 3 + Japan | 3 +(2 rows) + +SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id FOR ORDINALITY, "COUNTRY_NAME" text, "REGION_ID" int); + id | COUNTRY_NAME | REGION_ID +----+--------------+----------- + 1 | India | 3 + 2 | Japan | 3 +(2 rows) + +SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int); + id | COUNTRY_NAME | REGION_ID +----+--------------+----------- + 4 | India | 3 + 5 | Japan | 3 +(2 rows) + +SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id'); + id +---- + 4 + 5 +(2 rows) + +SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id FOR ORDINALITY); + id +---- + 1 + 2 +(2 rows) + +SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int, rawdata xml PATH '.'); + id | COUNTRY_NAME | REGION_ID | rawdata +----+--------------+-----------+------------------------------------------------------------------ + 4 | India | 3 | + + | | | IN + + | | | India + + | | | 3 + + | | | + 5 | Japan | 3 | + + | | | JP + + | | | Japan + + | | | 3Sinzo Abe+ + | | | +(2 rows) + +SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int, rawdata xml PATH './*'); + id | COUNTRY_NAME | REGION_ID | rawdata +----+--------------+-----------+----------------------------------------------------------------------------------------------------------------------------- + 4 | India | 3 | INIndia3 + 5 | Japan | 3 | JPJapan3Sinzo Abe +(2 rows) + +SELECT * FROM xmltable('/root' passing 'a1aa2a bbbbxxxcccc' COLUMNS element text); + element +------------------- + a1aa2a bbbbcccc +(1 row) + +SELECT * FROM xmltable('/root' passing 'a1aa2a bbbbxxxcccc' COLUMNS element text PATH 'element/text()'); -- should fail +ERROR: more than one value returned by column XPath expression +-- CDATA test +select * from xmltable('r' passing ' &"<>!foo]]>2' columns c text); + c +------------------------- + &"<>!foo + 2 +(2 rows) + +-- XML builtin entities +SELECT * FROM xmltable('/x/a' PASSING ''"&<>' COLUMNS ent text); + ent +----- + ' + " + & + < + > +(5 rows) + +SELECT * FROM xmltable('/x/a' PASSING ''"&<>' COLUMNS ent xml); + ent +------------------ + ' + " + & + < + > +(5 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT xmltable.* + FROM (SELECT data FROM xmldata) x, + LATERAL XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Nested Loop + Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name + -> Seq Scan on public.xmldata + Output: xmldata.data + -> Function Scan on "xmltable" + Output: "xmltable".id, "xmltable"._id, "xmltable".country_name, "xmltable".country_id, "xmltable".region_id, "xmltable".size, "xmltable".unit, "xmltable".premier_name + Function Call: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) +(7 rows) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified') FROM xmldata; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Seq Scan on public.xmldata + Output: XMLTABLE(('/ROWS/ROW'::text) PASSING (data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) +(2 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT (xmltable).* FROM + (SELECT XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified') FROM xmldata) s; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Subquery Scan on s + Output: (s."xmltable").id, (s."xmltable")._id, (s."xmltable").country_name, (s."xmltable").country_id, (s."xmltable").region_id, (s."xmltable").size, (s."xmltable").unit, (s."xmltable").premier_name + -> Seq Scan on public.xmldata + Output: XMLTABLE(('/ROWS/ROW'::text) PASSING (xmldata.data) COLUMNS id integer PATH ('@id'::text), _id FOR ORDINALITY, country_name text PATH ('COUNTRY_NAME'::text) NOT NULL, country_id text PATH ('COUNTRY_ID'::text), region_id integer PATH ('REGION_ID'::text), size double precision PATH ('SIZE'::text), unit text PATH ('SIZE/@unit'::text), premier_name text DEFAULT ('not specified'::text) PATH ('PREMIER_NAME'::text)) +(4 rows) + diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql index 08a0b30..11d7402 100644 --- a/src/test/regress/sql/xml.sql +++ b/src/test/regress/sql/xml.sql @@ -270,3 +270,173 @@ SELECT XMLPARSE(DOCUMENT ']> SELECT XMLPARSE(DOCUMENT ']>&c;'); -- This might or might not load the requested DTD, but it mustn't throw error. SELECT XMLPARSE(DOCUMENT ' '); + +-- XMLPATH tests +CREATE TABLE xmldata(data xml); +INSERT INTO xmldata VALUES(' + + AU + Australia + 3 + + + CN + China + 3 + + + HK + HongKong + 3 + + + IN + India + 3 + + + JP + Japan + 3Sinzo Abe + + + SG + Singapore + 3791 + +'); + +-- XMLTABLE without columns +SELECT * FROM XMLTABLE('/rows/row' PASSING '1020'); +SELECT XMLTABLE('/rows/row' PASSING '1020'); + +-- XMLTABLE with columns +SELECT xmltable.* + FROM (SELECT data FROM xmldata) x, + LATERAL XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); + +CREATE VIEW xmltableview1 AS SELECT xmltable.* + FROM (SELECT data FROM xmldata) x, + LATERAL XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); + +SELECT * FROM xmltableview1; + +\sv xmltableview1 + +EXPLAIN (COSTS OFF) SELECT * FROM xmltableview1; +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM xmltableview1; + +-- XMLNAMESPACES tests +SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS zz), + '/zz:rows/zz:row' + PASSING '10' + COLUMNS a int PATH 'zz:a'); + +CREATE VIEW xmltableview2 AS SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x.y' AS zz), + '/zz:rows/zz:row' + PASSING '10' + COLUMNS a int PATH 'zz:a'); + +SELECT * FROM xmltableview2; + +SELECT * FROM XMLTABLE(XMLNAMESPACES(DEFAULT 'http://x.y'), + '/rows/row' + PASSING '10' + COLUMNS a int PATH 'a'); + +SELECT * FROM XMLTABLE('/rows/row/a/text()' PASSING '1020'); + +-- used in prepare statements +PREPARE pp AS +SELECT xmltable.* + FROM (SELECT data FROM xmldata) x, + LATERAL XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); + +EXECUTE pp; + +SELECT xmltable('/ROWS/ROW' PASSING data) FROM xmldata; +SELECT xmltable('/ROWS/ROW[COUNTRY_NAME="Japan"]' PASSING data) FROM xmldata; +SELECT xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data) FROM xmldata; +SELECT (xmltable).* FROM (SELECT xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int) FROM xmldata) s; +SELECT xmltable.* FROM xmldata,LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS "COUNTRY_NAME" text, "REGION_ID" int); +SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id FOR ORDINALITY, "COUNTRY_NAME" text, "REGION_ID" int); +SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int); +SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id'); +SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id FOR ORDINALITY); +SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int, rawdata xml PATH '.'); +SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int, rawdata xml PATH './*'); + +SELECT * FROM xmltable('/root' passing 'a1aa2a bbbbxxxcccc' COLUMNS element text); +SELECT * FROM xmltable('/root' passing 'a1aa2a bbbbxxxcccc' COLUMNS element text PATH 'element/text()'); -- should fail + +-- CDATA test +select * from xmltable('r' passing ' &"<>!foo]]>2' columns c text); + +-- XML builtin entities +SELECT * FROM xmltable('/x/a' PASSING ''"&<>' COLUMNS ent text); +SELECT * FROM xmltable('/x/a' PASSING ''"&<>' COLUMNS ent xml); + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT xmltable.* + FROM (SELECT data FROM xmldata) x, + LATERAL XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); + +EXPLAIN (VERBOSE, COSTS OFF) SELECT XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified') FROM xmldata; + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT (xmltable).* FROM + (SELECT XMLTABLE('/ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + _id FOR ORDINALITY, + country_name text PATH 'COUNTRY_NAME' NOT NULL, + country_id text PATH 'COUNTRY_ID', + region_id int PATH 'REGION_ID', + size float PATH 'SIZE', + unit text PATH 'SIZE/@unit', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified') FROM xmldata) s;