diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml index d8d91678e86d4..c51c5ab754be4 100644 --- a/doc/src/sgml/datatype.sgml +++ b/doc/src/sgml/datatype.sgml @@ -243,6 +243,12 @@ autoincrementing four-byte integer + + sql_query + + pre-parsed, analyzed SQL query + + text @@ -5148,6 +5154,73 @@ WHERE ... + + <type>sql_query</type> Type + + + sql_query + + + + The sql_query data type stores a fully analyzed SQL query. + Its input function parses and semantically analyzes the input SQL text, + resolving all object names to object identifiers (OIDs) at that time. + Its output function deparsed the stored query back to canonical SQL text + using PostgreSQL's deparser, which is sensitive + to the current search_path: when the search path is + empty, all relation and type names are emitted in schema-qualified form. + + + + This type is used as the argument type for ts_stat + when the SQL query argument should be pre-validated and schema-qualified + at assignment time rather than at execution time. The canonical use case + is a materialized view body that calls ts_stat: when + PostgreSQL refreshes the view (e.g., via + pg_restore) with an empty search_path + for security, a text argument containing unqualified table + names would fail to resolve. A sql_query value instead + emits public.articles from its output function and the + query executes correctly. + + + + Internally, the value is stored in the same + nodeToString serialization format used by + pg_node_tree catalog columns. Unlike pg_node_tree, + sql_query accepts SQL text as input. The serialized format is + not intended to be stable across major PostgreSQL + versions; values are correctly round-tripped by pg_dump + because the dump writes the canonical SQL text produced by the output + function. + + + + The sql_query type supports the standard comparison operators + (=, <>, + <, <=, + >, >=) + and a B-tree operator class, which makes it usable in + ORDER BY clauses and indexes. Comparison is performed + on the canonical SQL text. + + + + A cast from text to sql_query is available but + requires an explicit cast (CAST(... AS sql_query) or + the ::sql_query notation). No implicit cast is + defined, to prevent accidental substitution. The reverse cast from + sql_query to text is implicit. + + + + ts_stat accepts a sql_query value + directly; no column definition list is needed because the output columns + are declared as named OUT parameters. + See for details. + + + Pseudo-Types diff --git a/doc/src/sgml/func/func-textsearch.sgml b/doc/src/sgml/func/func-textsearch.sgml index 290ad81d6979b..99c16d295c08f 100644 --- a/doc/src/sgml/func/func-textsearch.sgml +++ b/doc/src/sgml/func/func-textsearch.sgml @@ -1039,6 +1039,29 @@ (foo,10,15) ... + + + + ts_stat ( sqlquery sql_query + , weights text ) + setof record + ( word text, + ndoc integer, + nentry integer ) + + + Like the text form, but accepts a pre-parsed + sql_query value. The query is deparsed at execution + time using the current search_path, which ensures + that schema-qualified names are emitted when the search path is + empty (as during pg_restore's + REFRESH MATERIALIZED VIEW step). + + + ts_stat($$ SELECT vector FROM apod $$::sql_query) + (foo,10,15) ... + + diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index c54774b327590..11f96024ed3e5 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -84,9 +84,12 @@ #include "funcapi.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" +#include "nodes/nodes.h" +#include "nodes/parsenodes.h" #include "parser/parsetree.h" #include "rewrite/rewriteRemove.h" #include "storage/lmgr.h" +#include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/syscache.h" @@ -1943,6 +1946,31 @@ find_expr_references_walker(Node *node, context->addrs); break; + /* + * A sql_query constant holds a nodeToString-serialized + * analyzed Query. Recursively extract all objects + * referenced by that inner query so they become + * dependencies of the outer expression (e.g. a matview + * body). This is what lets pg_restore order REFRESH + * MATERIALIZED VIEW after the tables the query references. + */ + case SQL_QUERYOID: + { + char *nodestr; + Query *innerq; + + nodestr = TextDatumGetCString(con->constvalue); + innerq = castNode(Query, stringToNode(nodestr)); + pfree(nodestr); + + context->rtables = lcons(innerq->rtable, + context->rtables); + find_expr_references_walker((Node *) innerq, context); + context->rtables = + list_delete_first(context->rtables); + break; + } + /* * Dependencies for regrole should be shared among all * databases, so explicitly inhibit to have dependencies. diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build index d793f8145f6c2..e1bb125ee4379 100644 --- a/src/backend/utils/adt/meson.build +++ b/src/backend/utils/adt/meson.build @@ -102,6 +102,7 @@ backend_sources += files( 'ruleutils.c', 'selfuncs.c', 'skipsupport.c', + 'sqlquery.c', 'tid.c', 'timestamp.c', 'trigfuncs.c', diff --git a/src/backend/utils/adt/sqlquery.c b/src/backend/utils/adt/sqlquery.c new file mode 100644 index 0000000000000..4263084bd7bb3 --- /dev/null +++ b/src/backend/utils/adt/sqlquery.c @@ -0,0 +1,285 @@ +/*------------------------------------------------------------------------- + * + * sqlquery.c + * I/O and comparison functions for the sql_query data type. + * + * sql_query stores a fully analyzed SQL query tree in nodeToString() format + * (same wire representation as pg_node_tree columns such as pg_rewrite.ev_action). + * The external text representation is canonical SQL produced by pg_get_querydef(). + * + * Key property: because the stored datum holds an analyzed Query node with + * relation OIDs rather than unqualified names, deparsing in a context with an + * empty search_path (e.g. pg_restore's REFRESH MATERIALIZED VIEW security + * context) automatically emits schema-qualified names. This fixes the failure + * of ts_stat() inside materialized views during pg_restore. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/backend/utils/adt/sqlquery.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/hash.h" +#include "catalog/pg_collation.h" +#include "libpq/pqformat.h" +#include "nodes/nodes.h" +#include "nodes/parsenodes.h" +#include "nodes/pg_list.h" +#include "parser/parser.h" +#include "tcop/tcopprot.h" +#include "utils/builtins.h" +#include "utils/ruleutils.h" +#include "utils/varlena.h" + +PG_FUNCTION_INFO_V1(sql_query_in); +PG_FUNCTION_INFO_V1(sql_query_out); +PG_FUNCTION_INFO_V1(sql_query_recv); +PG_FUNCTION_INFO_V1(sql_query_send); +PG_FUNCTION_INFO_V1(sql_query_eq); +PG_FUNCTION_INFO_V1(sql_query_ne); +PG_FUNCTION_INFO_V1(sql_query_lt); +PG_FUNCTION_INFO_V1(sql_query_le); +PG_FUNCTION_INFO_V1(sql_query_gt); +PG_FUNCTION_INFO_V1(sql_query_ge); +PG_FUNCTION_INFO_V1(sql_query_cmp); +PG_FUNCTION_INFO_V1(sql_query_hash); +PG_FUNCTION_INFO_V1(sql_query_hash_extended); + + +/* + * sql_query_parse_and_analyze + * + * Parse and semantically analyze a SQL string, returning a palloc'd + * nodeToString() serialization of the resulting Query node. Errors on + * invalid SQL or on anything other than exactly one statement. + */ +static char * +sql_query_parse_and_analyze(const char *str) +{ + List *raw_list; + RawStmt *rawstmt; + List *query_list; + Query *query; + + raw_list = raw_parser(str, RAW_PARSE_DEFAULT); + + if (list_length(raw_list) != 1) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("sql_query value must contain exactly one SQL statement"))); + + rawstmt = linitial_node(RawStmt, raw_list); + + /* + * Analyze with no parameter types. Parameterized queries ($1, $2, …) + * are unsupported because the sql_query type carries no parameter type + * information. + */ + query_list = pg_analyze_and_rewrite_fixedparams(rawstmt, str, NULL, 0, NULL); + + if (list_length(query_list) != 1) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unexpected result from query analysis"))); + + query = linitial_node(Query, query_list); + + return nodeToString(query); +} + + +/* + * sql_query_in - input function + * + * Parse and analyze the SQL string; store the analyzed Query as nodeToString. + */ +Datum +sql_query_in(PG_FUNCTION_ARGS) +{ + char *str = PG_GETARG_CSTRING(0); + char *nodestr; + + nodestr = sql_query_parse_and_analyze(str); + PG_RETURN_TEXT_P(cstring_to_text(nodestr)); +} + + +/* + * sql_query_out - output function + * + * Deparse the stored Query node back to canonical SQL text. + * generate_relation_name() is search_path-sensitive: when called in a context + * with an empty search_path (e.g. pg_restore's REFRESH), it emits + * schema-qualified names, making the deparsed SQL safe to re-execute. + */ +Datum +sql_query_out(PG_FUNCTION_ARGS) +{ + text *val = PG_GETARG_TEXT_PP(0); + char *nodestr = text_to_cstring(val); + Query *query; + char *result; + + query = castNode(Query, stringToNode(nodestr)); + result = pg_get_querydef(query, false); + + PG_RETURN_CSTRING(result); +} + + +/* + * sql_query_recv - binary input function + * + * Read a SQL text string from the wire; parse and analyze it. + */ +Datum +sql_query_recv(PG_FUNCTION_ARGS) +{ + StringInfo buf = (StringInfo) PG_GETARG_POINTER(0); + char *str; + int nbytes; + char *nodestr; + + str = pq_getmsgtext(buf, buf->len - buf->cursor, &nbytes); + nodestr = sql_query_parse_and_analyze(str); + pfree(str); + + PG_RETURN_TEXT_P(cstring_to_text(nodestr)); +} + + +/* + * sql_query_send - binary output function + * + * Send the canonical SQL text representation (search_path-aware deparsing). + */ +Datum +sql_query_send(PG_FUNCTION_ARGS) +{ + text *val = PG_GETARG_TEXT_PP(0); + char *nodestr = text_to_cstring(val); + Query *query; + char *sql; + StringInfoData buf; + + query = castNode(Query, stringToNode(nodestr)); + sql = pg_get_querydef(query, false); + + pq_begintypsend(&buf); + pq_sendtext(&buf, sql, strlen(sql)); + PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); +} + + +/* + * sql_query_cmp - comparison function + * + * Compares the nodeToString representations of two sql_query values. + * Two queries that parse and analyze to the same Query node are equal. + * The ordering is consistent but not guaranteed to be human-meaningful. + */ +Datum +sql_query_cmp(PG_FUNCTION_ARGS) +{ + text *a = PG_GETARG_TEXT_PP(0); + text *b = PG_GETARG_TEXT_PP(1); + int result; + + /* + * Compare the nodeToString representations byte-for-byte using C + * collation. The stored format is always ASCII, and equality means + * identical serialized Query trees, so locale-sensitive ordering would be + * both meaningless and potentially inconsistent with the hash functions. + */ + result = varstr_cmp(VARDATA_ANY(a), VARSIZE_ANY_EXHDR(a), + VARDATA_ANY(b), VARSIZE_ANY_EXHDR(b), + C_COLLATION_OID); + + PG_FREE_IF_COPY(a, 0); + PG_FREE_IF_COPY(b, 1); + + PG_RETURN_INT32(result); +} + +Datum +sql_query_eq(PG_FUNCTION_ARGS) +{ + PG_RETURN_BOOL(DatumGetInt32(DirectFunctionCall2(sql_query_cmp, + PG_GETARG_DATUM(0), + PG_GETARG_DATUM(1))) == 0); +} + +Datum +sql_query_ne(PG_FUNCTION_ARGS) +{ + PG_RETURN_BOOL(DatumGetInt32(DirectFunctionCall2(sql_query_cmp, + PG_GETARG_DATUM(0), + PG_GETARG_DATUM(1))) != 0); +} + +Datum +sql_query_lt(PG_FUNCTION_ARGS) +{ + PG_RETURN_BOOL(DatumGetInt32(DirectFunctionCall2(sql_query_cmp, + PG_GETARG_DATUM(0), + PG_GETARG_DATUM(1))) < 0); +} + +Datum +sql_query_le(PG_FUNCTION_ARGS) +{ + PG_RETURN_BOOL(DatumGetInt32(DirectFunctionCall2(sql_query_cmp, + PG_GETARG_DATUM(0), + PG_GETARG_DATUM(1))) <= 0); +} + +Datum +sql_query_gt(PG_FUNCTION_ARGS) +{ + PG_RETURN_BOOL(DatumGetInt32(DirectFunctionCall2(sql_query_cmp, + PG_GETARG_DATUM(0), + PG_GETARG_DATUM(1))) > 0); +} + +Datum +sql_query_ge(PG_FUNCTION_ARGS) +{ + PG_RETURN_BOOL(DatumGetInt32(DirectFunctionCall2(sql_query_cmp, + PG_GETARG_DATUM(0), + PG_GETARG_DATUM(1))) >= 0); +} + +/* + * sql_query_hash / sql_query_hash_extended + * + * Hash support functions for the hash opclass. We hash the raw bytes of the + * nodeToString representation, consistent with the C-collation comparison + * used by sql_query_cmp. + */ +Datum +sql_query_hash(PG_FUNCTION_ARGS) +{ + text *val = PG_GETARG_TEXT_PP(0); + Datum result; + + result = hash_any((unsigned char *) VARDATA_ANY(val), + VARSIZE_ANY_EXHDR(val)); + PG_FREE_IF_COPY(val, 0); + PG_RETURN_DATUM(result); +} + +Datum +sql_query_hash_extended(PG_FUNCTION_ARGS) +{ + text *val = PG_GETARG_TEXT_PP(0); + uint64 seed = PG_GETARG_INT64(1); + Datum result; + + result = hash_any_extended((unsigned char *) VARDATA_ANY(val), + VARSIZE_ANY_EXHDR(val), seed); + PG_FREE_IF_COPY(val, 0); + PG_RETURN_DATUM(result); +} diff --git a/src/backend/utils/adt/tsvector_op.c b/src/backend/utils/adt/tsvector_op.c index 53a9541e89f6a..15e07510952e4 100644 --- a/src/backend/utils/adt/tsvector_op.c +++ b/src/backend/utils/adt/tsvector_op.c @@ -31,6 +31,7 @@ #include "utils/builtins.h" #include "utils/regproc.h" #include "utils/rel.h" +#include "utils/ruleutils.h" typedef struct @@ -2638,6 +2639,18 @@ ts_stat_sql(MemoryContext persistentContext, text *txt, text *ws) return stat; } +/* + * ts_stat1 / ts_stat2 + * + * ts_stat accepts a sql_query value. The datum holds an analyzed Query node + * in nodeToString() format. We deparse it to canonical SQL via + * pg_get_querydef(), which is search_path-sensitive: in a context with a + * restricted search_path (e.g. pg_restore's REFRESH MATERIALIZED VIEW), it + * emits schema-qualified names, making the re-executed query safe. + */ +PG_FUNCTION_INFO_V1(ts_stat1); +PG_FUNCTION_INFO_V1(ts_stat2); + Datum ts_stat1(PG_FUNCTION_ARGS) { @@ -2647,12 +2660,19 @@ ts_stat1(PG_FUNCTION_ARGS) if (SRF_IS_FIRSTCALL()) { TSVectorStat *stat; - text *txt = PG_GETARG_TEXT_PP(0); + text *sqldatum = PG_GETARG_TEXT_PP(0); + Query *query; + char *sql; + text *sqltxt; + + query = castNode(Query, stringToNode(text_to_cstring(sqldatum))); + sql = pg_get_querydef(query, false); + sqltxt = cstring_to_text(sql); funcctx = SRF_FIRSTCALL_INIT(); SPI_connect(); - stat = ts_stat_sql(funcctx->multi_call_memory_ctx, txt, NULL); - PG_FREE_IF_COPY(txt, 0); + stat = ts_stat_sql(funcctx->multi_call_memory_ctx, sqltxt, NULL); + pfree(sqltxt); ts_setup_firstcall(fcinfo, funcctx, stat); SPI_finish(); } @@ -2672,13 +2692,20 @@ ts_stat2(PG_FUNCTION_ARGS) if (SRF_IS_FIRSTCALL()) { TSVectorStat *stat; - text *txt = PG_GETARG_TEXT_PP(0); + text *sqldatum = PG_GETARG_TEXT_PP(0); text *ws = PG_GETARG_TEXT_PP(1); + Query *query; + char *sql; + text *sqltxt; + + query = castNode(Query, stringToNode(text_to_cstring(sqldatum))); + sql = pg_get_querydef(query, false); + sqltxt = cstring_to_text(sql); funcctx = SRF_FIRSTCALL_INIT(); SPI_connect(); - stat = ts_stat_sql(funcctx->multi_call_memory_ctx, txt, ws); - PG_FREE_IF_COPY(txt, 0); + stat = ts_stat_sql(funcctx->multi_call_memory_ctx, sqltxt, ws); + pfree(sqltxt); PG_FREE_IF_COPY(ws, 1); ts_setup_firstcall(fcinfo, funcctx, stat); SPI_finish(); diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 04eb2af2ac870..2891f63d345cc 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202606232 +#define CATALOG_VERSION_NO 202606253 #endif diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat index 8d5a0004a478a..9d15a67e5d674 100644 --- a/src/include/catalog/pg_amop.dat +++ b/src/include/catalog/pg_amop.dat @@ -3250,4 +3250,26 @@ amoprighttype => 'point', amopstrategy => '7', amopopr => '@>(box,point)', amopmethod => 'brin' }, +# btree sql_query_ops +{ amopfamily => 'btree/sql_query_ops', amoplefttype => 'sql_query', + amoprighttype => 'sql_query', amopstrategy => '1', + amopopr => '<(sql_query,sql_query)', amopmethod => 'btree' }, +{ amopfamily => 'btree/sql_query_ops', amoplefttype => 'sql_query', + amoprighttype => 'sql_query', amopstrategy => '2', + amopopr => '<=(sql_query,sql_query)', amopmethod => 'btree' }, +{ amopfamily => 'btree/sql_query_ops', amoplefttype => 'sql_query', + amoprighttype => 'sql_query', amopstrategy => '3', + amopopr => '=(sql_query,sql_query)', amopmethod => 'btree' }, +{ amopfamily => 'btree/sql_query_ops', amoplefttype => 'sql_query', + amoprighttype => 'sql_query', amopstrategy => '4', + amopopr => '>=(sql_query,sql_query)', amopmethod => 'btree' }, +{ amopfamily => 'btree/sql_query_ops', amoplefttype => 'sql_query', + amoprighttype => 'sql_query', amopstrategy => '5', + amopopr => '>(sql_query,sql_query)', amopmethod => 'btree' }, + +# hash sql_query_ops +{ amopfamily => 'hash/sql_query_ops', amoplefttype => 'sql_query', + amoprighttype => 'sql_query', amopstrategy => '1', + amopopr => '=(sql_query,sql_query)', amopmethod => 'hash' }, + ] diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat index 4a1efdbc89986..2becdaf88c694 100644 --- a/src/include/catalog/pg_amproc.dat +++ b/src/include/catalog/pg_amproc.dat @@ -2036,4 +2036,16 @@ { amprocfamily => 'brin/box_inclusion_ops', amproclefttype => 'box', amprocrighttype => 'box', amprocnum => '13', amproc => 'box_contain' }, +# btree sql_query_ops support functions +{ amprocfamily => 'btree/sql_query_ops', amproclefttype => 'sql_query', + amprocrighttype => 'sql_query', amprocnum => '1', amproc => 'sql_query_cmp' }, +{ amprocfamily => 'btree/sql_query_ops', amproclefttype => 'sql_query', + amprocrighttype => 'sql_query', amprocnum => '4', amproc => 'btequalimage' }, + +# hash sql_query_ops support functions +{ amprocfamily => 'hash/sql_query_ops', amproclefttype => 'sql_query', + amprocrighttype => 'sql_query', amprocnum => '1', amproc => 'sql_query_hash' }, +{ amprocfamily => 'hash/sql_query_ops', amproclefttype => 'sql_query', + amprocrighttype => 'sql_query', amprocnum => '2', amproc => 'sql_query_hash_extended' }, + ] diff --git a/src/include/catalog/pg_cast.dat b/src/include/catalog/pg_cast.dat index a7b6d812c5ac9..5e01f3d9edd0c 100644 --- a/src/include/catalog/pg_cast.dat +++ b/src/include/catalog/pg_cast.dat @@ -594,4 +594,10 @@ { castsource => 'tstzrange', casttarget => 'tstzmultirange', castfunc => 'tstzmultirange(tstzrange)', castcontext => 'e', castmethod => 'f' }, + +# sql_query casts: implicit from text (parses and analyzes); implicit to text (deparsed SQL) +{ castsource => 'text', casttarget => 'sql_query', castfunc => '0', + castcontext => 'i', castmethod => 'i' }, +{ castsource => 'sql_query', casttarget => 'text', castfunc => '0', + castcontext => 'i', castmethod => 'i' }, ] diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat index df170b80840bb..c9215bc196c48 100644 --- a/src/include/catalog/pg_opclass.dat +++ b/src/include/catalog/pg_opclass.dat @@ -484,6 +484,11 @@ opcfamily => 'brin/pg_lsn_bloom_ops', opcintype => 'pg_lsn', opcdefault => 'f', opckeytype => 'pg_lsn' }, +{ opcmethod => 'btree', opcname => 'sql_query_ops', + opcfamily => 'btree/sql_query_ops', opcintype => 'sql_query' }, +{ opcmethod => 'hash', opcname => 'sql_query_ops', + opcfamily => 'hash/sql_query_ops', opcintype => 'sql_query' }, + # no brin opclass for enum, tsvector, tsquery, jsonb { opcmethod => 'brin', opcname => 'box_inclusion_ops', diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat index c7f860c442b3d..06b743a57df7e 100644 --- a/src/include/catalog/pg_operator.dat +++ b/src/include/catalog/pg_operator.dat @@ -830,6 +830,38 @@ oprcom => '<=(text,text)', oprnegate => '<(text,text)', oprcode => 'text_ge', oprrest => 'scalargesel', oprjoin => 'scalargejoinsel' }, +# sql_query comparison operators +{ oid => '8809', descr => 'equal', + oprname => '=', oprcanmerge => 't', oprcanhash => 't', + oprleft => 'sql_query', oprright => 'sql_query', oprresult => 'bool', + oprcom => '=(sql_query,sql_query)', oprnegate => '<>(sql_query,sql_query)', + oprcode => 'sql_query_eq', oprrest => 'eqsel', oprjoin => 'eqjoinsel' }, +{ oid => '8810', descr => 'not equal', + oprname => '<>', oprleft => 'sql_query', oprright => 'sql_query', + oprresult => 'bool', oprcom => '<>(sql_query,sql_query)', + oprnegate => '=(sql_query,sql_query)', oprcode => 'sql_query_ne', + oprrest => 'neqsel', oprjoin => 'neqjoinsel' }, +{ oid => '8811', descr => 'less than', + oprname => '<', oprleft => 'sql_query', oprright => 'sql_query', + oprresult => 'bool', oprcom => '>(sql_query,sql_query)', + oprnegate => '>=(sql_query,sql_query)', oprcode => 'sql_query_lt', + oprrest => 'scalarltsel', oprjoin => 'scalarltjoinsel' }, +{ oid => '8812', descr => 'less than or equal', + oprname => '<=', oprleft => 'sql_query', oprright => 'sql_query', + oprresult => 'bool', oprcom => '>=(sql_query,sql_query)', + oprnegate => '>(sql_query,sql_query)', oprcode => 'sql_query_le', + oprrest => 'scalarlesel', oprjoin => 'scalarlejoinsel' }, +{ oid => '8813', descr => 'greater than', + oprname => '>', oprleft => 'sql_query', oprright => 'sql_query', + oprresult => 'bool', oprcom => '<(sql_query,sql_query)', + oprnegate => '<=(sql_query,sql_query)', oprcode => 'sql_query_gt', + oprrest => 'scalargtsel', oprjoin => 'scalargtjoinsel' }, +{ oid => '8814', descr => 'greater than or equal', + oprname => '>=', oprleft => 'sql_query', oprright => 'sql_query', + oprresult => 'bool', oprcom => '<=(sql_query,sql_query)', + oprnegate => '<(sql_query,sql_query)', oprcode => 'sql_query_ge', + oprrest => 'scalargesel', oprjoin => 'scalargejoinsel' }, + { oid => '670', descr => 'equal', oprname => '=', oprcanmerge => 't', oprcanhash => 't', oprleft => 'float8', oprright => 'float8', oprresult => 'bool', oprcom => '=(float8,float8)', diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat index c6110183103aa..72655738a12f7 100644 --- a/src/include/catalog/pg_opfamily.dat +++ b/src/include/catalog/pg_opfamily.dat @@ -308,5 +308,9 @@ opfmethod => 'hash', opfname => 'multirange_ops' }, { oid => '6158', opfmethod => 'gist', opfname => 'multirange_ops' }, +{ oid => '8815', + opfmethod => 'btree', opfname => 'sql_query_ops' }, +{ oid => '8818', + opfmethod => 'hash', opfname => 'sql_query_ops' }, ] diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa76c7923f0c3..8cd8153c7f6de 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -581,6 +581,47 @@ proname => 'pg_node_tree_send', provolatile => 's', prorettype => 'bytea', proargtypes => 'pg_node_tree', prosrc => 'pg_node_tree_send' }, +# sql_query type I/O and comparison functions +{ oid => '8796', descr => 'I/O', + proname => 'sql_query_in', provolatile => 's', prorettype => 'sql_query', + proargtypes => 'cstring', prosrc => 'sql_query_in' }, +{ oid => '8797', descr => 'I/O', + proname => 'sql_query_out', provolatile => 's', prorettype => 'cstring', + proargtypes => 'sql_query', prosrc => 'sql_query_out' }, +{ oid => '8798', descr => 'I/O', + proname => 'sql_query_recv', provolatile => 's', prorettype => 'sql_query', + proargtypes => 'internal', prosrc => 'sql_query_recv' }, +{ oid => '8799', descr => 'I/O', + proname => 'sql_query_send', provolatile => 's', prorettype => 'bytea', + proargtypes => 'sql_query', prosrc => 'sql_query_send' }, +{ oid => '8800', descr => 'equal', + proname => 'sql_query_eq', prorettype => 'bool', + proargtypes => 'sql_query sql_query', prosrc => 'sql_query_eq' }, +{ oid => '8801', descr => 'not equal', + proname => 'sql_query_ne', prorettype => 'bool', + proargtypes => 'sql_query sql_query', prosrc => 'sql_query_ne' }, +{ oid => '8802', descr => 'less than', + proname => 'sql_query_lt', prorettype => 'bool', + proargtypes => 'sql_query sql_query', prosrc => 'sql_query_lt' }, +{ oid => '8803', descr => 'less than or equal', + proname => 'sql_query_le', prorettype => 'bool', + proargtypes => 'sql_query sql_query', prosrc => 'sql_query_le' }, +{ oid => '8804', descr => 'greater than', + proname => 'sql_query_gt', prorettype => 'bool', + proargtypes => 'sql_query sql_query', prosrc => 'sql_query_gt' }, +{ oid => '8805', descr => 'greater than or equal', + proname => 'sql_query_ge', prorettype => 'bool', + proargtypes => 'sql_query sql_query', prosrc => 'sql_query_ge' }, +{ oid => '8806', descr => 'less-equal-greater', + proname => 'sql_query_cmp', prorettype => 'int4', + proargtypes => 'sql_query sql_query', prosrc => 'sql_query_cmp' }, +{ oid => '8816', descr => 'hash', + proname => 'sql_query_hash', prorettype => 'int4', + proargtypes => 'sql_query', prosrc => 'sql_query_hash' }, +{ oid => '8817', descr => 'hash', + proname => 'sql_query_hash_extended', prorettype => 'int8', + proargtypes => 'sql_query int8', prosrc => 'sql_query_hash_extended' }, + # OIDS 200 - 299 { oid => '200', descr => 'I/O', @@ -10070,16 +10111,16 @@ proname => 'ts_typanalyze', provolatile => 's', prorettype => 'bool', proargtypes => 'internal', prosrc => 'ts_typanalyze' }, -{ oid => '3689', descr => 'statistics of tsvector column', +{ oid => '8807', descr => 'statistics of tsvector column from pre-parsed query', proname => 'ts_stat', procost => '10', prorows => '10000', proretset => 't', provolatile => 'v', proparallel => 'u', prorettype => 'record', - proargtypes => 'text', proallargtypes => '{text,text,int4,int4}', + proargtypes => 'sql_query', proallargtypes => '{sql_query,text,int4,int4}', proargmodes => '{i,o,o,o}', proargnames => '{query,word,ndoc,nentry}', prosrc => 'ts_stat1' }, -{ oid => '3690', descr => 'statistics of tsvector column', +{ oid => '8808', descr => 'statistics of tsvector column from pre-parsed query', proname => 'ts_stat', procost => '10', prorows => '10000', proretset => 't', provolatile => 'v', proparallel => 'u', prorettype => 'record', - proargtypes => 'text text', proallargtypes => '{text,text,text,int4,int4}', + proargtypes => 'sql_query text', proallargtypes => '{sql_query,text,text,int4,int4}', proargmodes => '{i,i,o,o,o}', proargnames => '{query,weights,word,ndoc,nentry}', prosrc => 'ts_stat2' }, diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat index 42ee494601ba1..71ec300d5394b 100644 --- a/src/include/catalog/pg_type.dat +++ b/src/include/catalog/pg_type.dat @@ -149,6 +149,12 @@ typoutput => 'pg_node_tree_out', typreceive => 'pg_node_tree_recv', typsend => 'pg_node_tree_send', typalign => 'i', typstorage => 'x', typcollation => 'default' }, +{ oid => '8794', array_type_oid => '8795', + descr => 'pre-parsed, analyzed SQL query', + typname => 'sql_query', typlen => '-1', typbyval => 'f', + typcategory => 'Z', typinput => 'sql_query_in', + typoutput => 'sql_query_out', typreceive => 'sql_query_recv', + typsend => 'sql_query_send', typalign => 'i', typstorage => 'x' }, { oid => '3361', descr => 'multivariate ndistinct coefficients', typname => 'pg_ndistinct', typlen => '-1', typbyval => 'f', typcategory => 'Z', typinput => 'pg_ndistinct_in', diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out index 80400a3373473..3ca8bd9764a98 100644 --- a/src/test/regress/expected/opr_sanity.out +++ b/src/test/regress/expected/opr_sanity.out @@ -1428,7 +1428,13 @@ ORDER BY 1; 3940 | jsonb_extract_path_text | get value from jsonb as text with path elements 3951 | json_extract_path | get value from json with path elements 3953 | json_extract_path_text | get value from json as text with path elements -(9 rows) + 8800 | sql_query_eq | equal + 8801 | sql_query_ne | not equal + 8802 | sql_query_lt | less than + 8803 | sql_query_le | less than or equal + 8804 | sql_query_gt | greater than + 8805 | sql_query_ge | greater than or equal +(15 rows) -- Operators that are commutator pairs should have identical volatility -- and leakproofness markings on their implementation functions. diff --git a/src/test/regress/expected/sqlquery.out b/src/test/regress/expected/sqlquery.out new file mode 100644 index 0000000000000..d8e1afe32e143 --- /dev/null +++ b/src/test/regress/expected/sqlquery.out @@ -0,0 +1,244 @@ +-- +-- Tests for the sql_query data type +-- +-- sql_query stores a fully analyzed SQL query in nodeToString() format. +-- Its key property is that deparsing under an empty search_path emits +-- schema-qualified names, fixing pg_restore's REFRESH MATERIALIZED VIEW +-- failure when ts_stat() references unqualified table names. +-- +-- Verify type and operator catalog entries +SELECT typname, typcategory, typinput::regproc, typoutput::regproc +FROM pg_type WHERE typname = 'sql_query'; + typname | typcategory | typinput | typoutput +-----------+-------------+--------------+--------------- + sql_query | Z | sql_query_in | sql_query_out +(1 row) + +SELECT oprname, oprleft::regtype, oprright::regtype, oprresult::regtype +FROM pg_operator WHERE oprleft = 'sql_query'::regtype +ORDER BY oprname; + oprname | oprleft | oprright | oprresult +---------+-----------+-----------+----------- + < | sql_query | sql_query | boolean + <= | sql_query | sql_query | boolean + <> | sql_query | sql_query | boolean + = | sql_query | sql_query | boolean + > | sql_query | sql_query | boolean + >= | sql_query | sql_query | boolean +(6 rows) + +SELECT amopstrategy, amoplefttype::regtype, amoprighttype::regtype +FROM pg_amop +WHERE amopfamily = ( + SELECT oid FROM pg_opfamily + WHERE opfmethod = (SELECT oid FROM pg_am WHERE amname = 'btree') + AND opfname = 'sql_query_ops' +) +ORDER BY amopstrategy; + amopstrategy | amoplefttype | amoprighttype +--------------+--------------+--------------- + 1 | sql_query | sql_query + 2 | sql_query | sql_query + 3 | sql_query | sql_query + 4 | sql_query | sql_query + 5 | sql_query | sql_query +(5 rows) + +-- I/O: canonical SQL produced by pg_get_querydef() +SELECT 'SELECT 1 + 1'::sql_query; + sql_query +------------------------------- + SELECT (1 + 1) AS "?column?" +(1 row) + +SELECT 'SELECT 1+1'::sql_query; -- normalised to same form + sql_query +------------------------------- + SELECT (1 + 1) AS "?column?" +(1 row) + +SELECT 'select 1+1'::sql_query; -- case-insensitive SQL keyword + sql_query +------------------------------- + SELECT (1 + 1) AS "?column?" +(1 row) + +-- Multiple-statement input is rejected +SELECT 'SELECT 1; SELECT 2'::sql_query; +ERROR: sql_query value must contain exactly one SQL statement +LINE 1: SELECT 'SELECT 1; SELECT 2'::sql_query; + ^ +-- Invalid SQL is rejected +SELECT 'NOT VALID SQL'::sql_query; +ERROR: syntax error at or near "NOT" +LINE 1: SELECT 'NOT VALID SQL'::sql_query; + ^ +-- Non-SELECT statements are accepted (any fully-parseable statement) +SELECT 'UPDATE pg_class SET relname = relname WHERE false'::sql_query; + sql_query +---------------------------------------- + UPDATE pg_class SET relname = relname+ + WHERE false +(1 row) + +-- Arrays of sql_query +SELECT ARRAY['SELECT 1'::sql_query, 'SELECT 2'::sql_query]; + array +----------------------------------------------------------- + {" SELECT 1 AS \"?column?\""," SELECT 2 AS \"?column?\""} +(1 row) + +-- Equality and comparison operators +SELECT 'SELECT 1 + 1'::sql_query = 'SELECT 1+1'::sql_query AS equal; + equal +------- + t +(1 row) + +SELECT 'SELECT 1'::sql_query = 'SELECT 2'::sql_query AS not_equal; + not_equal +----------- + f +(1 row) + +SELECT 'SELECT 1'::sql_query < 'SELECT 2'::sql_query AS less_than; + less_than +----------- + t +(1 row) + +SELECT 'SELECT 2'::sql_query > 'SELECT 1'::sql_query AS greater_than; + greater_than +-------------- + t +(1 row) + +-- ORDER BY uses the B-tree opclass +SELECT q FROM + (VALUES ('SELECT 3'::sql_query), ('SELECT 1'::sql_query), ('SELECT 2'::sql_query)) v(q) +ORDER BY q; + q +------------------------- + SELECT 1 AS "?column?" + SELECT 2 AS "?column?" + SELECT 3 AS "?column?" +(3 rows) + +-- DISTINCT: synonymous queries collapse to one row +SELECT DISTINCT q FROM + (VALUES ('SELECT 1+1'::sql_query), ('SELECT 1 + 1'::sql_query), ('SELECT 2'::sql_query)) v(q) +ORDER BY q; + q +------------------------------- + SELECT 2 AS "?column?" + SELECT (1 + 1) AS "?column?" +(2 rows) + +-- text ↔ sql_query casts: both directions are implicit +SELECT 'SELECT 1'::sql_query::text; -- sql_query → text (implicit) + text +------------------------- + SELECT 1 AS "?column?" +(1 row) + +SELECT CAST('SELECT 1' AS sql_query); -- text → sql_query (implicit) + sql_query +------------------------- + SELECT 1 AS "?column?" +(1 row) + +-- ts_stat with sql_query +-- +-- String literals are untyped; with only ts_stat(sql_query) available, +-- function type resolution coerces them to sql_query automatically via +-- sql_query_in(). No explicit cast is needed, preserving backward +-- compatibility with existing ts_stat('...') call sites. +DROP TABLE IF EXISTS sqlquery_articles; +NOTICE: table "sqlquery_articles" does not exist, skipping +CREATE TABLE sqlquery_articles (body text); +INSERT INTO sqlquery_articles VALUES + ('the cat sat on the mat'), + ('cats are great'); +SELECT word, ndoc, nentry +FROM ts_stat( + $$ SELECT to_tsvector('english', body) FROM sqlquery_articles $$ +) +ORDER BY ndoc DESC, word; + word | ndoc | nentry +-------+------+-------- + cat | 2 | 2 + great | 1 | 1 + mat | 1 | 1 + sat | 1 | 1 +(4 rows) + +-- Weights filter: 'abcd' includes all weights including the default 'D' +SELECT word, ndoc, nentry +FROM ts_stat( + $$ SELECT to_tsvector('english', body) FROM sqlquery_articles $$, + 'abcd' +) +ORDER BY ndoc DESC, word; + word | ndoc | nentry +-------+------+-------- + cat | 2 | 2 + great | 1 | 1 + mat | 1 | 1 + sat | 1 | 1 +(4 rows) + +-- Materialized view using ts_stat(sql_query) and its dependencies +-- +-- After the matview is created, the dependency tracker must have recorded +-- sqlquery_articles as a dependency of the rewrite rule (via the sql_query +-- constant in the ts_stat() argument). +CREATE MATERIALIZED VIEW sqlquery_word_stats AS + SELECT word, ndoc, nentry + FROM ts_stat( + $$ SELECT to_tsvector('english', body) FROM sqlquery_articles $$ + ) + ORDER BY ndoc DESC, word; +SELECT * FROM sqlquery_word_stats; + word | ndoc | nentry +-------+------+-------- + cat | 2 | 2 + great | 1 | 1 + mat | 1 | 1 + sat | 1 | 1 +(4 rows) + +-- Dependency must include sqlquery_articles +SELECT d.refobjid::regclass AS dep +FROM pg_rewrite r +JOIN pg_depend d ON d.classid = 'pg_rewrite'::regclass AND d.objid = r.oid +WHERE r.ev_class = 'sqlquery_word_stats'::regclass + AND d.refclassid = 'pg_class'::regclass + AND d.refobjid <> 'sqlquery_word_stats'::regclass +ORDER BY dep; + dep +------------------- + sqlquery_articles +(1 row) + +-- Primary fix: REFRESH under empty search_path must succeed. +-- +-- pg_restore uses an empty search_path when it runs REFRESH MATERIALIZED VIEW +-- for security. With the old text-based ts_stat, the unqualified table name +-- "sqlquery_articles" cannot be resolved in that context. With sql_query, +-- pg_get_querydef() emits the schema-qualified name "public.sqlquery_articles" +-- so SPI_prepare() succeeds. +SET search_path = ''; +REFRESH MATERIALIZED VIEW public.sqlquery_word_stats; +RESET search_path; +SELECT * FROM sqlquery_word_stats; + word | ndoc | nentry +-------+------+-------- + cat | 2 | 2 + great | 1 | 1 + mat | 1 | 1 + sat | 1 | 1 +(4 rows) + +-- Cleanup +DROP MATERIALIZED VIEW sqlquery_word_stats; +DROP TABLE sqlquery_articles; diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out index 1d21d3eb44678..fcc4d5029e65f 100644 --- a/src/test/regress/expected/type_sanity.out +++ b/src/test/regress/expected/type_sanity.out @@ -787,7 +787,8 @@ CREATE TABLE tab_core_types AS SELECT '(2020-01-02 03:04:05, 2021-02-03 06:07:08)'::tsrange, '{(2020-01-02 03:04:05, 2021-02-03 06:07:08)}'::tsmultirange, '(2020-01-02 03:04:05, 2021-02-03 06:07:08)'::tstzrange, - '{(2020-01-02 03:04:05, 2021-02-03 06:07:08)}'::tstzmultirange; + '{(2020-01-02 03:04:05, 2021-02-03 06:07:08)}'::tstzmultirange, + 'SELECT 1'::sql_query; -- Sanity check on the previous table, checking that all core types are -- included in this table. SELECT oid, typname, typtype, typelem, typarray diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 8fa0a6c47fb30..0016aaf30b79c 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -102,7 +102,7 @@ test: publication subscription # Another group of parallel tests # select_views depends on create_view # ---------- -test: select_views portals_p2 foreign_key dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite graph_table +test: select_views portals_p2 foreign_key dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite graph_table sqlquery # ---------- # Another group of parallel tests (JSON related) diff --git a/src/test/regress/sql/sqlquery.sql b/src/test/regress/sql/sqlquery.sql new file mode 100644 index 0000000000000..a0487b19cb324 --- /dev/null +++ b/src/test/regress/sql/sqlquery.sql @@ -0,0 +1,144 @@ +-- +-- Tests for the sql_query data type +-- +-- sql_query stores a fully analyzed SQL query in nodeToString() format. +-- Its key property is that deparsing under an empty search_path emits +-- schema-qualified names, fixing pg_restore's REFRESH MATERIALIZED VIEW +-- failure when ts_stat() references unqualified table names. +-- + +-- Verify type and operator catalog entries + +SELECT typname, typcategory, typinput::regproc, typoutput::regproc +FROM pg_type WHERE typname = 'sql_query'; + +SELECT oprname, oprleft::regtype, oprright::regtype, oprresult::regtype +FROM pg_operator WHERE oprleft = 'sql_query'::regtype +ORDER BY oprname; + +SELECT amopstrategy, amoplefttype::regtype, amoprighttype::regtype +FROM pg_amop +WHERE amopfamily = ( + SELECT oid FROM pg_opfamily + WHERE opfmethod = (SELECT oid FROM pg_am WHERE amname = 'btree') + AND opfname = 'sql_query_ops' +) +ORDER BY amopstrategy; + +-- I/O: canonical SQL produced by pg_get_querydef() + +SELECT 'SELECT 1 + 1'::sql_query; +SELECT 'SELECT 1+1'::sql_query; -- normalised to same form +SELECT 'select 1+1'::sql_query; -- case-insensitive SQL keyword + +-- Multiple-statement input is rejected + +SELECT 'SELECT 1; SELECT 2'::sql_query; + +-- Invalid SQL is rejected + +SELECT 'NOT VALID SQL'::sql_query; + +-- Non-SELECT statements are accepted (any fully-parseable statement) + +SELECT 'UPDATE pg_class SET relname = relname WHERE false'::sql_query; + +-- Arrays of sql_query + +SELECT ARRAY['SELECT 1'::sql_query, 'SELECT 2'::sql_query]; + +-- Equality and comparison operators + +SELECT 'SELECT 1 + 1'::sql_query = 'SELECT 1+1'::sql_query AS equal; +SELECT 'SELECT 1'::sql_query = 'SELECT 2'::sql_query AS not_equal; +SELECT 'SELECT 1'::sql_query < 'SELECT 2'::sql_query AS less_than; +SELECT 'SELECT 2'::sql_query > 'SELECT 1'::sql_query AS greater_than; + +-- ORDER BY uses the B-tree opclass + +SELECT q FROM + (VALUES ('SELECT 3'::sql_query), ('SELECT 1'::sql_query), ('SELECT 2'::sql_query)) v(q) +ORDER BY q; + +-- DISTINCT: synonymous queries collapse to one row + +SELECT DISTINCT q FROM + (VALUES ('SELECT 1+1'::sql_query), ('SELECT 1 + 1'::sql_query), ('SELECT 2'::sql_query)) v(q) +ORDER BY q; + +-- text ↔ sql_query casts: both directions are implicit + +SELECT 'SELECT 1'::sql_query::text; -- sql_query → text (implicit) +SELECT CAST('SELECT 1' AS sql_query); -- text → sql_query (implicit) + +-- ts_stat with sql_query +-- +-- String literals are untyped; with only ts_stat(sql_query) available, +-- function type resolution coerces them to sql_query automatically via +-- sql_query_in(). No explicit cast is needed, preserving backward +-- compatibility with existing ts_stat('...') call sites. + +DROP TABLE IF EXISTS sqlquery_articles; +CREATE TABLE sqlquery_articles (body text); +INSERT INTO sqlquery_articles VALUES + ('the cat sat on the mat'), + ('cats are great'); + +SELECT word, ndoc, nentry +FROM ts_stat( + $$ SELECT to_tsvector('english', body) FROM sqlquery_articles $$ +) +ORDER BY ndoc DESC, word; + +-- Weights filter: 'abcd' includes all weights including the default 'D' + +SELECT word, ndoc, nentry +FROM ts_stat( + $$ SELECT to_tsvector('english', body) FROM sqlquery_articles $$, + 'abcd' +) +ORDER BY ndoc DESC, word; + +-- Materialized view using ts_stat(sql_query) and its dependencies +-- +-- After the matview is created, the dependency tracker must have recorded +-- sqlquery_articles as a dependency of the rewrite rule (via the sql_query +-- constant in the ts_stat() argument). + +CREATE MATERIALIZED VIEW sqlquery_word_stats AS + SELECT word, ndoc, nentry + FROM ts_stat( + $$ SELECT to_tsvector('english', body) FROM sqlquery_articles $$ + ) + ORDER BY ndoc DESC, word; + +SELECT * FROM sqlquery_word_stats; + +-- Dependency must include sqlquery_articles + +SELECT d.refobjid::regclass AS dep +FROM pg_rewrite r +JOIN pg_depend d ON d.classid = 'pg_rewrite'::regclass AND d.objid = r.oid +WHERE r.ev_class = 'sqlquery_word_stats'::regclass + AND d.refclassid = 'pg_class'::regclass + AND d.refobjid <> 'sqlquery_word_stats'::regclass +ORDER BY dep; + +-- Primary fix: REFRESH under empty search_path must succeed. +-- +-- pg_restore uses an empty search_path when it runs REFRESH MATERIALIZED VIEW +-- for security. With the old text-based ts_stat, the unqualified table name +-- "sqlquery_articles" cannot be resolved in that context. With sql_query, +-- pg_get_querydef() emits the schema-qualified name "public.sqlquery_articles" +-- so SPI_prepare() succeeds. + +SET search_path = ''; +REFRESH MATERIALIZED VIEW public.sqlquery_word_stats; +RESET search_path; + +SELECT * FROM sqlquery_word_stats; + +-- Cleanup + +DROP MATERIALIZED VIEW sqlquery_word_stats; +DROP TABLE sqlquery_articles; diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql index 95d5b6e09151a..0774592da7c14 100644 --- a/src/test/regress/sql/type_sanity.sql +++ b/src/test/regress/sql/type_sanity.sql @@ -603,7 +603,8 @@ CREATE TABLE tab_core_types AS SELECT '(2020-01-02 03:04:05, 2021-02-03 06:07:08)'::tsrange, '{(2020-01-02 03:04:05, 2021-02-03 06:07:08)}'::tsmultirange, '(2020-01-02 03:04:05, 2021-02-03 06:07:08)'::tstzrange, - '{(2020-01-02 03:04:05, 2021-02-03 06:07:08)}'::tstzmultirange; + '{(2020-01-02 03:04:05, 2021-02-03 06:07:08)}'::tstzmultirange, + 'SELECT 1'::sql_query; -- Sanity check on the previous table, checking that all core types are -- included in this table.